diff --git a/packages/react-native/Libraries/Components/ScrollView/ScrollView.d.ts b/packages/react-native/Libraries/Components/ScrollView/ScrollView.d.ts index b6e538ddc5a1be..69ed2871652597 100644 --- a/packages/react-native/Libraries/Components/ScrollView/ScrollView.d.ts +++ b/packages/react-native/Libraries/Components/ScrollView/ScrollView.d.ts @@ -526,6 +526,50 @@ export interface ScrollViewPropsIOS { | ((event: NativeSyntheticEvent) => void) | undefined; + /** + * The style of the top edge effect. Available on iOS 26.0 and later. + * @platform ios + */ + topEdgeEffect?: + | { + style?: 'automatic' | 'soft' | 'hard' | undefined; + hidden?: boolean | undefined; + } + | undefined; + + /** + * The style of the bottom edge effect. Available on iOS 26.0 and later. + * @platform ios + */ + bottomEdgeEffect?: + | { + style?: 'automatic' | 'soft' | 'hard' | undefined; + hidden?: boolean | undefined; + } + | undefined; + + /** + * The style of the left edge effect. Available on iOS 26.0 and later. + * @platform ios + */ + leftEdgeEffect?: + | { + style?: 'automatic' | 'soft' | 'hard' | undefined; + hidden?: boolean | undefined; + } + | undefined; + + /** + * The style of the right edge effect. Available on iOS 26.0 and later. + * @platform ios + */ + rightEdgeEffect?: + | { + style?: 'automatic' | 'soft' | 'hard' | undefined; + hidden?: boolean | undefined; + } + | undefined; + /** * The current scale of the scroll view content. The default value is 1.0. */ diff --git a/packages/react-native/Libraries/Components/ScrollView/ScrollViewNativeComponent.js b/packages/react-native/Libraries/Components/ScrollView/ScrollViewNativeComponent.js index 82f9c93baf348c..446467c85168e9 100644 --- a/packages/react-native/Libraries/Components/ScrollView/ScrollViewNativeComponent.js +++ b/packages/react-native/Libraries/Components/ScrollView/ScrollViewNativeComponent.js @@ -156,6 +156,10 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = snapToInterval: true, snapToOffsets: true, snapToStart: true, + topEdgeEffect: true, + bottomEdgeEffect: true, + leftEdgeEffect: true, + rightEdgeEffect: true, verticalScrollIndicatorInsets: { diff: require('../../Utilities/differ/insetsDiffer').default, }, diff --git a/packages/react-native/Libraries/Components/ScrollView/ScrollViewNativeComponentType.js b/packages/react-native/Libraries/Components/ScrollView/ScrollViewNativeComponentType.js index 1feee6999541c0..edb60acc9c8784 100644 --- a/packages/react-native/Libraries/Components/ScrollView/ScrollViewNativeComponentType.js +++ b/packages/react-native/Libraries/Components/ScrollView/ScrollViewNativeComponentType.js @@ -77,6 +77,22 @@ export type ScrollViewNativeProps = $ReadOnly<{ snapToInterval?: ?number, snapToOffsets?: ?$ReadOnlyArray, snapToStart?: ?boolean, + topEdgeEffect?: ?$ReadOnly<{ + style?: ?('automatic' | 'soft' | 'hard'), + hidden?: ?boolean, + }>, + bottomEdgeEffect?: ?$ReadOnly<{ + style?: ?('automatic' | 'soft' | 'hard'), + hidden?: ?boolean, + }>, + leftEdgeEffect?: ?$ReadOnly<{ + style?: ?('automatic' | 'soft' | 'hard'), + hidden?: ?boolean, + }>, + rightEdgeEffect?: ?$ReadOnly<{ + style?: ?('automatic' | 'soft' | 'hard'), + hidden?: ?boolean, + }>, zoomScale?: ?number, // Overrides onResponderGrant?: ?(e: GestureResponderEvent) => void | boolean, diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm index 15e75f45632a60..98e024941c3a3f 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm @@ -58,6 +58,21 @@ static UIScrollViewIndicatorStyle RCTUIScrollViewIndicatorStyleFromProps(const S } } +#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 260000 +API_AVAILABLE(ios(26.0)) +static UIScrollEdgeEffectStyle *RCTUIScrollEdgeEffectStyleFromProps(ScrollViewEdgeEffectStyle style) +{ + switch (style) { + case ScrollViewEdgeEffectStyle::Automatic: + return UIScrollEdgeEffectStyle.automaticStyle; + case ScrollViewEdgeEffectStyle::Soft: + return UIScrollEdgeEffectStyle.softStyle; + case ScrollViewEdgeEffectStyle::Hard: + return UIScrollEdgeEffectStyle.hardStyle; + } +} +#endif + // Once Fabric implements proper NativeAnimationDriver, this should be removed. // This is just a workaround to allow animations based on onScroll event. // This is only used to animate sticky headers in ScrollViews, and only the contentOffset and tag is used. @@ -445,6 +460,43 @@ - (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 >= 260000 + if (@available(iOS 26.0, *)) { + if (oldScrollViewProps.topEdgeEffect != newScrollViewProps.topEdgeEffect) { + if (newScrollViewProps.topEdgeEffect.has_value()) { + if (newScrollViewProps.topEdgeEffect->style.has_value()) { + _scrollView.topEdgeEffect.style = RCTUIScrollEdgeEffectStyleFromProps(newScrollViewProps.topEdgeEffect->style.value()); + } + _scrollView.topEdgeEffect.hidden = newScrollViewProps.topEdgeEffect->hidden; + } + } + if (oldScrollViewProps.bottomEdgeEffect != newScrollViewProps.bottomEdgeEffect) { + if (newScrollViewProps.bottomEdgeEffect.has_value()) { + if (newScrollViewProps.bottomEdgeEffect->style.has_value()) { + _scrollView.bottomEdgeEffect.style = RCTUIScrollEdgeEffectStyleFromProps(newScrollViewProps.bottomEdgeEffect->style.value()); + } + _scrollView.bottomEdgeEffect.hidden = newScrollViewProps.bottomEdgeEffect->hidden; + } + } + if (oldScrollViewProps.leftEdgeEffect != newScrollViewProps.leftEdgeEffect) { + if (newScrollViewProps.leftEdgeEffect.has_value()) { + if (newScrollViewProps.leftEdgeEffect->style.has_value()) { + _scrollView.leftEdgeEffect.style = RCTUIScrollEdgeEffectStyleFromProps(newScrollViewProps.leftEdgeEffect->style.value()); + } + _scrollView.leftEdgeEffect.hidden = newScrollViewProps.leftEdgeEffect->hidden; + } + } + if (oldScrollViewProps.rightEdgeEffect != newScrollViewProps.rightEdgeEffect) { + if (newScrollViewProps.rightEdgeEffect.has_value()) { + if (newScrollViewProps.rightEdgeEffect->style.has_value()) { + _scrollView.rightEdgeEffect.style = RCTUIScrollEdgeEffectStyleFromProps(newScrollViewProps.rightEdgeEffect->style.value()); + } + _scrollView.rightEdgeEffect.hidden = newScrollViewProps.rightEdgeEffect->hidden; + } + } + } +#endif + [super updateProps:props oldProps:oldProps]; } 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 a683cb87f82566..ed8e816be5cb7a 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/scrollview/BaseScrollViewProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/scrollview/BaseScrollViewProps.cpp @@ -372,6 +372,42 @@ BaseScrollViewProps::BaseScrollViewProps( rawProps, "isInvertedVirtualizedList", sourceProps.isInvertedVirtualizedList, + {})), + topEdgeEffect( + ReactNativeFeatureFlags::enableCppPropsIteratorSetter() + ? sourceProps.topEdgeEffect + : convertRawProp( + context, + rawProps, + "topEdgeEffect", + sourceProps.topEdgeEffect, + {})), + bottomEdgeEffect( + ReactNativeFeatureFlags::enableCppPropsIteratorSetter() + ? sourceProps.bottomEdgeEffect + : convertRawProp( + context, + rawProps, + "bottomEdgeEffect", + sourceProps.bottomEdgeEffect, + {})), + leftEdgeEffect( + ReactNativeFeatureFlags::enableCppPropsIteratorSetter() + ? sourceProps.leftEdgeEffect + : convertRawProp( + context, + rawProps, + "leftEdgeEffect", + sourceProps.leftEdgeEffect, + {})), + rightEdgeEffect( + ReactNativeFeatureFlags::enableCppPropsIteratorSetter() + ? sourceProps.rightEdgeEffect + : convertRawProp( + context, + rawProps, + "rightEdgeEffect", + sourceProps.rightEdgeEffect, {})) {} void BaseScrollViewProps::setProp( @@ -425,6 +461,10 @@ void BaseScrollViewProps::setProp( RAW_SET_PROP_SWITCH_CASE_BASIC(contentInsetAdjustmentBehavior); RAW_SET_PROP_SWITCH_CASE_BASIC(scrollToOverflowEnabled); RAW_SET_PROP_SWITCH_CASE_BASIC(isInvertedVirtualizedList); + RAW_SET_PROP_SWITCH_CASE_BASIC(topEdgeEffect); + RAW_SET_PROP_SWITCH_CASE_BASIC(bottomEdgeEffect); + RAW_SET_PROP_SWITCH_CASE_BASIC(leftEdgeEffect); + RAW_SET_PROP_SWITCH_CASE_BASIC(rightEdgeEffect); } } @@ -559,7 +599,23 @@ SharedDebugStringConvertibleList BaseScrollViewProps::getDebugProps() const { debugStringConvertibleItem( "isInvertedVirtualizedList", isInvertedVirtualizedList, - defaultScrollViewProps.isInvertedVirtualizedList)}; + defaultScrollViewProps.isInvertedVirtualizedList), + debugStringConvertibleItem( + "topEdgeEffect", + topEdgeEffect, + defaultScrollViewProps.topEdgeEffect), + debugStringConvertibleItem( + "bottomEdgeEffect", + bottomEdgeEffect, + defaultScrollViewProps.bottomEdgeEffect), + debugStringConvertibleItem( + "leftEdgeEffect", + leftEdgeEffect, + defaultScrollViewProps.leftEdgeEffect), + debugStringConvertibleItem( + "rightEdgeEffect", + rightEdgeEffect, + defaultScrollViewProps.rightEdgeEffect)}; } #endif 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 f6de1cd07fd58a..f5ba0a68740573 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/scrollview/BaseScrollViewProps.h +++ b/packages/react-native/ReactCommon/react/renderer/components/scrollview/BaseScrollViewProps.h @@ -68,6 +68,10 @@ class BaseScrollViewProps : public ViewProps { ContentInsetAdjustmentBehavior contentInsetAdjustmentBehavior{ContentInsetAdjustmentBehavior::Never}; bool scrollToOverflowEnabled{false}; bool isInvertedVirtualizedList{false}; + std::optional topEdgeEffect{}; + std::optional bottomEdgeEffect{}; + std::optional leftEdgeEffect{}; + std::optional rightEdgeEffect{}; #pragma mark - DebugStringConvertible diff --git a/packages/react-native/ReactCommon/react/renderer/components/scrollview/conversions.h b/packages/react-native/ReactCommon/react/renderer/components/scrollview/conversions.h index fa0171ae1c1cfe..5f598023e25541 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/scrollview/conversions.h +++ b/packages/react-native/ReactCommon/react/renderer/components/scrollview/conversions.h @@ -93,6 +93,37 @@ fromRawValue(const PropsParserContext &context, const RawValue &value, ContentIn abort(); } +inline void fromRawValue(const PropsParserContext &context, const RawValue &value, ScrollViewEdgeEffectStyle &result) +{ + auto string = (std::string)value; + if (string == "automatic") { + result = ScrollViewEdgeEffectStyle::Automatic; + return; + } + if (string == "soft") { + result = ScrollViewEdgeEffectStyle::Soft; + return; + } + if (string == "hard") { + result = ScrollViewEdgeEffectStyle::Hard; + return; + } + abort(); +} + +inline void fromRawValue(const PropsParserContext &context, const RawValue &value, ScrollViewEdgeEffect &result) +{ + auto map = (std::unordered_map)value; + auto iterator = map.find("style"); + if (iterator != map.end()) { + fromRawValue(context, iterator->second, result.style.emplace()); + } + iterator = map.find("hidden"); + if (iterator != map.end()) { + fromRawValue(context, iterator->second, result.hidden); + } +} + inline void fromRawValue(const PropsParserContext &context, const RawValue &value, ScrollViewMaintainVisibleContentPosition &result) { @@ -146,6 +177,38 @@ inline std::string toString(const ScrollViewKeyboardDismissMode &value) } } +inline std::string toString(const ScrollViewEdgeEffectStyle &value) +{ + switch (value) { + case ScrollViewEdgeEffectStyle::Automatic: + return "automatic"; + case ScrollViewEdgeEffectStyle::Soft: + return "soft"; + case ScrollViewEdgeEffectStyle::Hard: + return "hard"; + } +} + +inline std::string toString(const ScrollViewEdgeEffect &value) +{ + std::string result = "{style: "; + if (value.style.has_value()) { + result += toString(value.style.value()); + } else { + result += "null"; + } + result += ", hidden: " + std::string(value.hidden ? "true" : "false") + "}"; + return result; +} + +inline std::string toString(const std::optional &value) +{ + if (!value) { + return "null"; + } + return toString(value.value()); +} + inline std::string toString(const ContentInsetAdjustmentBehavior &value) { switch (value) { diff --git a/packages/react-native/ReactCommon/react/renderer/components/scrollview/primitives.h b/packages/react-native/ReactCommon/react/renderer/components/scrollview/primitives.h index 32fdefb2e2f874..b31d31ca0aac00 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/scrollview/primitives.h +++ b/packages/react-native/ReactCommon/react/renderer/components/scrollview/primitives.h @@ -20,6 +20,25 @@ enum class ScrollViewKeyboardDismissMode { None, OnDrag, Interactive }; enum class ContentInsetAdjustmentBehavior { Never, Automatic, ScrollableAxes, Always }; +enum class ScrollViewEdgeEffectStyle { Automatic, Soft, Hard }; + +class ScrollViewEdgeEffect final { + public: + std::optional style{}; + bool hidden{false}; + + bool operator==(const ScrollViewEdgeEffect &rhs) const + { + return std::tie(this->style, this->hidden) == + std::tie(rhs.style, rhs.hidden); + } + + bool operator!=(const ScrollViewEdgeEffect &rhs) const + { + return !(*this == rhs); + } +}; + class ScrollViewMaintainVisibleContentPosition final { public: int minIndexForVisible{0}; diff --git a/packages/react-native/ReactNativeApi.d.ts b/packages/react-native/ReactNativeApi.d.ts index be0534addca38f..97779d92b17de1 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<<9504863966549f8efbbe4e13d7f74eaf>> + * @generated SignedSource<<5fcbfee6ee47ae0869e3fe332133e443>> * * This file was generated by scripts/js-api/build-types/index.js. */ @@ -4445,6 +4445,10 @@ declare type ScrollViewNativeProps = Readonly< automaticallyAdjustContentInsets?: boolean automaticallyAdjustKeyboardInsets?: boolean automaticallyAdjustsScrollIndicatorInsets?: boolean + bottomEdgeEffect?: { + readonly hidden?: boolean + readonly style?: "automatic" | "hard" | "soft" + } bounces?: boolean bouncesZoom?: boolean canCancelContentTouches?: boolean @@ -4469,6 +4473,10 @@ declare type ScrollViewNativeProps = Readonly< indicatorStyle?: "black" | "default" | "white" isInvertedVirtualizedList?: boolean keyboardDismissMode?: "interactive" | "none" | "on-drag" + leftEdgeEffect?: { + readonly hidden?: boolean + readonly style?: "automatic" | "hard" | "soft" + } maintainVisibleContentPosition?: { readonly autoscrollToTopThreshold?: number readonly minIndexForVisible: number @@ -4486,6 +4494,10 @@ declare type ScrollViewNativeProps = Readonly< pagingEnabled?: boolean persistentScrollbar?: boolean pinchGestureEnabled?: boolean + rightEdgeEffect?: { + readonly hidden?: boolean + readonly style?: "automatic" | "hard" | "soft" + } scrollEnabled?: boolean scrollEventThrottle?: number scrollIndicatorInsets?: EdgeInsetsProp @@ -4500,6 +4512,10 @@ declare type ScrollViewNativeProps = Readonly< snapToInterval?: number snapToOffsets?: ReadonlyArray snapToStart?: boolean + topEdgeEffect?: { + readonly hidden?: boolean + readonly style?: "automatic" | "hard" | "soft" + } zoomScale?: number onScrollToTop?: (event: ScrollEvent) => void } @@ -5973,7 +5989,7 @@ export { AlertOptions, // a0cdac0f AlertType, // 5ab91217 AndroidKeyboardEvent, // e03becc8 - Animated, // 47139b9b + Animated, // 98afa783 AppConfig, // ebddad4b AppRegistry, // 6cdee1d6 AppState, // 12012be5 @@ -6019,7 +6035,7 @@ export { EventSubscription, // b8d084aa ExtendedExceptionData, // 5a6ccf5a FilterFunction, // bf24c0e3 - FlatList, // a6600e49 + FlatList, // 6e35a861 FlatListProps, // de2108a1 FocusEvent, // 529b43eb FontVariant, // 7c7558bb