From 9da12978022e665f835e6c66215272d235c95849 Mon Sep 17 00:00:00 2001 From: Artur Kalach Date: Sat, 2 May 2026 15:31:52 +0200 Subject: [PATCH 1/2] feat: add allowsKeyboardScrolling prop to ScrollView for native api support --- .../Components/ScrollView/ScrollView.d.ts | 7 ++++ .../Components/ScrollView/ScrollView.js | 8 +++++ .../ScrollView/ScrollViewNativeComponent.js | 1 + .../ScrollViewNativeComponentType.js | 1 + .../__snapshots__/ScrollView-test.js.snap | 1 + .../__snapshots__/FlatList-test.js.snap | 2 ++ .../ScrollView/RCTScrollViewComponentView.mm | 13 +++++++ .../React/Views/ScrollView/RCTScrollView.m | 5 +++ .../Views/ScrollView/RCTScrollViewManager.m | 1 + .../scrollview/BaseScrollViewProps.cpp | 14 ++++++++ .../scrollview/BaseScrollViewProps.h | 1 + .../HostPlatformScrollViewProps.cpp | 4 +++ packages/react-native/ReactNativeApi.d.ts | 24 +++++++------ .../examples/ScrollView/ScrollViewExample.js | 34 +++++++++++++++++++ 14 files changed, 105 insertions(+), 11 deletions(-) diff --git a/packages/react-native/Libraries/Components/ScrollView/ScrollView.d.ts b/packages/react-native/Libraries/Components/ScrollView/ScrollView.d.ts index bd9f15396e75..1508f39b1cb0 100644 --- a/packages/react-native/Libraries/Components/ScrollView/ScrollView.d.ts +++ b/packages/react-native/Libraries/Components/ScrollView/ScrollView.d.ts @@ -334,6 +334,13 @@ interface ScrollResponderMixin extends SubscribableMixin { } export interface ScrollViewPropsIOS { + /** + * When true, the scroll view allows scrolling its content with hardware + * keyboard input. The default value is true. Available on iOS 17 and later. + * @platform ios + * @see https://developer.apple.com/documentation/uikit/uiscrollview/allowskeyboardscrolling + */ + allowsKeyboardScrolling?: boolean | undefined; /** * When true the scroll view bounces horizontally when it reaches the end * even if the content is smaller than the scroll view itself. The default diff --git a/packages/react-native/Libraries/Components/ScrollView/ScrollView.js b/packages/react-native/Libraries/Components/ScrollView/ScrollView.js index c8392ea6e461..8af906e479e1 100644 --- a/packages/react-native/Libraries/Components/ScrollView/ScrollView.js +++ b/packages/react-native/Libraries/Components/ScrollView/ScrollView.js @@ -175,6 +175,13 @@ export interface PublicScrollViewInstance type InnerViewInstance = React.ElementRef; export type ScrollViewPropsIOS = Readonly<{ + /** + * When true, the scroll view allows scrolling its content with hardware + * keyboard input. The default value is true. Available on iOS 17 and later. + * @platform ios + * @see https://developer.apple.com/documentation/uikit/uiscrollview/allowskeyboardscrolling + */ + allowsKeyboardScrolling?: boolean, /** * Controls whether iOS should automatically adjust the content inset * for scroll views that are placed behind a navigation bar or @@ -1769,6 +1776,7 @@ class ScrollView extends React.Component { } = this.props; const props = { ...otherProps, + allowsKeyboardScrolling: this.props.allowsKeyboardScrolling !== false, alwaysBounceHorizontal, alwaysBounceVertical, style: StyleSheet.compose(baseStyle, this.props.style), diff --git a/packages/react-native/Libraries/Components/ScrollView/ScrollViewNativeComponent.js b/packages/react-native/Libraries/Components/ScrollView/ScrollViewNativeComponent.js index eeee1a603e72..b65965ba0264 100644 --- a/packages/react-native/Libraries/Components/ScrollView/ScrollViewNativeComponent.js +++ b/packages/react-native/Libraries/Components/ScrollView/ScrollViewNativeComponent.js @@ -104,6 +104,7 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = }, }, validAttributes: { + allowsKeyboardScrolling: true, alwaysBounceHorizontal: true, alwaysBounceVertical: true, automaticallyAdjustContentInsets: true, diff --git a/packages/react-native/Libraries/Components/ScrollView/ScrollViewNativeComponentType.js b/packages/react-native/Libraries/Components/ScrollView/ScrollViewNativeComponentType.js index 9cd18a3d2bac..4b8983c5583e 100644 --- a/packages/react-native/Libraries/Components/ScrollView/ScrollViewNativeComponentType.js +++ b/packages/react-native/Libraries/Components/ScrollView/ScrollViewNativeComponentType.js @@ -21,6 +21,7 @@ import type {ViewProps} from '../View/ViewPropTypes'; export type ScrollViewNativeProps = Readonly<{ ...ViewProps, + allowsKeyboardScrolling?: ?boolean, alwaysBounceHorizontal?: ?boolean, alwaysBounceVertical?: ?boolean, automaticallyAdjustContentInsets?: ?boolean, diff --git a/packages/react-native/Libraries/Components/ScrollView/__tests__/__snapshots__/ScrollView-test.js.snap b/packages/react-native/Libraries/Components/ScrollView/__tests__/__snapshots__/ScrollView-test.js.snap index 44636eb9dabe..dd2cd36d246b 100644 --- a/packages/react-native/Libraries/Components/ScrollView/__tests__/__snapshots__/ScrollView-test.js.snap +++ b/packages/react-native/Libraries/Components/ScrollView/__tests__/__snapshots__/ScrollView-test.js.snap @@ -14,6 +14,7 @@ exports[`ScrollView renders its children: should deep render when mocked (please exports[`ScrollView renders its children: should deep render when not mocked (please verify output manually) 1`] = ` getClipsContentToBounds(); _scrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; _scrollView.delaysContentTouches = NO; + #if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 170000 /* __IPHONE_17_0 */ + if (@available(iOS 17.0, *)) { + _scrollView.allowsKeyboardScrolling = YES; + } + #endif ((RCTEnhancedScrollView *)_scrollView).overridingDelegate = self; _isUserTriggeredScrolling = NO; _shouldUpdateContentInsetAdjustmentBehavior = YES; @@ -447,6 +452,14 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & scrollView.keyboardDismissMode = RCTUIKeyboardDismissModeFromProps(newScrollViewProps); } + #if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 170000 /* __IPHONE_17_0 */ + if (@available(iOS 17.0, *)) { + if (oldScrollViewProps.allowsKeyboardScrolling != newScrollViewProps.allowsKeyboardScrolling) { + scrollView.allowsKeyboardScrolling = newScrollViewProps.allowsKeyboardScrolling; + } + } + #endif + [super updateProps:props oldProps:oldProps]; } diff --git a/packages/react-native/React/Views/ScrollView/RCTScrollView.m b/packages/react-native/React/Views/ScrollView/RCTScrollView.m index 72c7c39e3c1a..6eef7c922847 100644 --- a/packages/react-native/React/Views/ScrollView/RCTScrollView.m +++ b/packages/react-native/React/Views/ScrollView/RCTScrollView.m @@ -540,6 +540,11 @@ - (void)updateClippedSubviews } } +- (void)setAllowsKeyboardScrolling:(BOOL)allowsKeyboardScrolling API_AVAILABLE(ios(17.0)) +{ + _scrollView.allowsKeyboardScrolling = allowsKeyboardScrolling; +} + - (void)setContentInset:(UIEdgeInsets)contentInset { if (UIEdgeInsetsEqualToEdgeInsets(contentInset, _contentInset)) { diff --git a/packages/react-native/React/Views/ScrollView/RCTScrollViewManager.m b/packages/react-native/React/Views/ScrollView/RCTScrollViewManager.m index 985771805c59..324f06c15bb4 100644 --- a/packages/react-native/React/Views/ScrollView/RCTScrollViewManager.m +++ b/packages/react-native/React/Views/ScrollView/RCTScrollViewManager.m @@ -104,6 +104,7 @@ - (UIView *)view RCT_EXPORT_VIEW_PROPERTY(inverted, BOOL) RCT_EXPORT_VIEW_PROPERTY(automaticallyAdjustsScrollIndicatorInsets, BOOL) RCT_EXPORT_VIEW_PROPERTY(contentInsetAdjustmentBehavior, UIScrollViewContentInsetAdjustmentBehavior) +RCT_EXPORT_VIEW_PROPERTY(allowsKeyboardScrolling, BOOL) // overflow is used both in css-layout as well as by react-native. In css-layout // we always want to treat overflow as scroll but depending on what the overflow diff --git a/packages/react-native/ReactCommon/react/renderer/components/scrollview/BaseScrollViewProps.cpp b/packages/react-native/ReactCommon/react/renderer/components/scrollview/BaseScrollViewProps.cpp index a683cb87f825..fa20cf8e5187 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/scrollview/BaseScrollViewProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/scrollview/BaseScrollViewProps.cpp @@ -22,6 +22,15 @@ BaseScrollViewProps::BaseScrollViewProps( const BaseScrollViewProps& sourceProps, const RawProps& rawProps) : ViewProps(context, sourceProps, rawProps), + allowsKeyboardScrolling( + ReactNativeFeatureFlags::enableCppPropsIteratorSetter() + ? sourceProps.allowsKeyboardScrolling + : convertRawProp( + context, + rawProps, + "allowsKeyboardScrolling", + sourceProps.allowsKeyboardScrolling, + {})), alwaysBounceHorizontal( ReactNativeFeatureFlags::enableCppPropsIteratorSetter() ? sourceProps.alwaysBounceHorizontal @@ -387,6 +396,7 @@ void BaseScrollViewProps::setProp( static auto defaults = BaseScrollViewProps{}; switch (hash) { + RAW_SET_PROP_SWITCH_CASE_BASIC(allowsKeyboardScrolling); RAW_SET_PROP_SWITCH_CASE_BASIC(alwaysBounceHorizontal); RAW_SET_PROP_SWITCH_CASE_BASIC(alwaysBounceVertical); RAW_SET_PROP_SWITCH_CASE_BASIC(bounces); @@ -436,6 +446,10 @@ SharedDebugStringConvertibleList BaseScrollViewProps::getDebugProps() const { return ViewProps::getDebugProps() + SharedDebugStringConvertibleList{ + debugStringConvertibleItem( + "allowsKeyboardScrolling", + allowsKeyboardScrolling, + defaultScrollViewProps.allowsKeyboardScrolling), debugStringConvertibleItem( "alwaysBounceHorizontal", alwaysBounceHorizontal, diff --git a/packages/react-native/ReactCommon/react/renderer/components/scrollview/BaseScrollViewProps.h b/packages/react-native/ReactCommon/react/renderer/components/scrollview/BaseScrollViewProps.h index f6de1cd07fd5..95ab50b749e4 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/scrollview/BaseScrollViewProps.h +++ b/packages/react-native/ReactCommon/react/renderer/components/scrollview/BaseScrollViewProps.h @@ -29,6 +29,7 @@ class BaseScrollViewProps : public ViewProps { #pragma mark - Props + bool allowsKeyboardScrolling{true}; bool alwaysBounceHorizontal{}; bool alwaysBounceVertical{}; bool bounces{true}; diff --git a/packages/react-native/ReactCommon/react/renderer/components/scrollview/platform/android/react/renderer/components/scrollview/HostPlatformScrollViewProps.cpp b/packages/react-native/ReactCommon/react/renderer/components/scrollview/platform/android/react/renderer/components/scrollview/HostPlatformScrollViewProps.cpp index 7ef93d229526..b800e83b126f 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/scrollview/platform/android/react/renderer/components/scrollview/HostPlatformScrollViewProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/scrollview/platform/android/react/renderer/components/scrollview/HostPlatformScrollViewProps.cpp @@ -156,6 +156,10 @@ folly::dynamic HostPlatformScrollViewProps::getDiffProps( folly::dynamic result = ViewProps::getDiffProps(oldProps); + if (allowsKeyboardScrolling != oldProps->allowsKeyboardScrolling) { + result["allowsKeyboardScrolling"] = allowsKeyboardScrolling; + } + if (alwaysBounceHorizontal != oldProps->alwaysBounceHorizontal) { result["alwaysBounceHorizontal"] = alwaysBounceHorizontal; } diff --git a/packages/react-native/ReactNativeApi.d.ts b/packages/react-native/ReactNativeApi.d.ts index 13c53f3f59ab..b1cbdcae3e8c 100644 --- a/packages/react-native/ReactNativeApi.d.ts +++ b/packages/react-native/ReactNativeApi.d.ts @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<1c8637ab03a5fec9d39704d1ae305595>> + * @generated SignedSource<> * * This file was generated by scripts/js-api/build-types/index.js. */ @@ -4505,6 +4505,7 @@ declare interface ScrollViewImperativeMethods { declare type ScrollViewNativeComponent = typeof $$ScrollViewNativeComponent declare type ScrollViewNativeProps = Readonly< Omit & { + allowsKeyboardScrolling?: boolean alwaysBounceHorizontal?: boolean alwaysBounceVertical?: boolean automaticallyAdjustContentInsets?: boolean @@ -4588,6 +4589,7 @@ declare type ScrollViewPropsAndroid = { readonly scrollsChildToFocus?: boolean } declare type ScrollViewPropsIOS = { + readonly allowsKeyboardScrolling?: boolean readonly alwaysBounceHorizontal?: boolean readonly alwaysBounceVertical?: boolean readonly automaticallyAdjustContentInsets?: boolean @@ -6064,7 +6066,7 @@ export { AlertOptions, // a0cdac0f AlertType, // 5ab91217 AndroidKeyboardEvent, // e03becc8 - Animated, // 0652b5d1 + Animated, // 52117bcb AppConfig, // ce4209a7 AppRegistry, // 5edf0524 AppState, // 12012be5 @@ -6111,8 +6113,8 @@ export { EventSubscription, // b8d084aa ExtendedExceptionData, // 5a6ccf5a FilterFunction, // bf24c0e3 - FlatList, // 8c50f04a - FlatListProps, // e170f2c9 + FlatList, // 39da788d + FlatListProps, // 12e2c439 FocusEvent, // 62fc1eb8 FontVariant, // 7c7558bb GestureResponderEvent, // f693e9a5 @@ -6249,16 +6251,16 @@ export { ScrollEvent, // 5d529218 ScrollResponderType, // c6860ec8 ScrollToLocationParamsType, // d7ecdad1 - ScrollView, // a3918d1a + ScrollView, // 147623b2 ScrollViewImperativeMethods, // 7cd8d8de - ScrollViewProps, // 429fdd65 + ScrollViewProps, // f25d7fbd ScrollViewPropsAndroid, // 44210553 - ScrollViewPropsIOS, // b34b696c + ScrollViewPropsIOS, // bf4b8a52 ScrollViewScrollToOptions, // 3313411e SectionBase, // b376bddc - SectionList, // 92031230 + SectionList, // a1b818ef SectionListData, // 119baf83 - SectionListProps, // c0d0a46a + SectionListProps, // 7b5b438c SectionListRenderItem, // 1fad0435 SectionListRenderItemInfo, // 745e1992 Separators, // 6a45f7e3 @@ -6323,9 +6325,9 @@ export { ViewStyle, // 00a0f8fb VirtualViewMode, // 6be59722 VirtualizedList, // 68c7345e - VirtualizedListProps, // c7e8e7d7 + VirtualizedListProps, // 161a7bb2 VirtualizedSectionList, // 9fd9cd61 - VirtualizedSectionListProps, // 53a7e6a4 + VirtualizedSectionListProps, // a9c5ff7e WrapperComponentProvider, // 9cf3844c codegenNativeCommands, // 628a7c0a codegenNativeComponent, // 2baac257 diff --git a/packages/rn-tester/js/examples/ScrollView/ScrollViewExample.js b/packages/rn-tester/js/examples/ScrollView/ScrollViewExample.js index 11abb88cde7a..a81d2279fdf9 100644 --- a/packages/rn-tester/js/examples/ScrollView/ScrollViewExample.js +++ b/packages/rn-tester/js/examples/ScrollView/ScrollViewExample.js @@ -530,6 +530,14 @@ if (Platform.OS === 'ios') { return ; }, }); + examples.push({ + title: ' allowsKeyboardScrolling\n', + description: + 'When true, the scroll view allows scrolling its content with hardware keyboard input. The default value is true. Available on iOS 17 and later.', + render(): React.Node { + return ; + }, + }); } else if (Platform.OS === 'android') { examples.push({ title: ' EndFillColor & FadingEdgeLength\n', @@ -610,6 +618,32 @@ const ScrollsChildToFocusExample = () => { ); }; +const AllowsKeyboardScrollingExample = () => { + const [allowsKeyboardScrolling, setAllowsKeyboardScrolling] = useState(true); + return ( + + + Connect a hardware keyboard and use Page Up, Page Down, Home, or End + keys to scroll. + + + + {ITEMS.map(createItemRow)} + +