import React, { forwardRef } from "react"; import { Platform, View } from "react-native"; import Reanimated, { useAnimatedProps, useSharedValue, } from "react-native-reanimated"; import { ClippingScrollView } from "../../bindings"; import styles from "./styles"; import type { ScrollViewProps } from "react-native"; import type { SharedValue } from "react-native-reanimated"; const OS = Platform.OS; const ReanimatedClippingScrollView = OS === "android" ? Reanimated.createAnimatedComponent(ClippingScrollView) : ClippingScrollView; type AnimatedScrollViewProps = React.ComponentProps< typeof Reanimated.ScrollView >; export type AnimatedScrollViewComponent = React.ForwardRefExoticComponent< AnimatedScrollViewProps & React.RefAttributes >; type ScrollViewWithBottomPaddingProps = { ScrollViewComponent: AnimatedScrollViewComponent; children?: React.ReactNode; inverted?: boolean; bottomPadding: SharedValue; /** Padding for scroll indicator insets (excludes blankSpace). Falls back to bottomPadding when not provided. */ scrollIndicatorPadding?: SharedValue; /** Absolute Y content offset (iOS only, for KeyboardChatScrollView). */ contentOffsetY?: SharedValue; applyWorkaroundForContentInsetHitTestBug?: boolean; } & ScrollViewProps; const ScrollViewWithBottomPadding = forwardRef< Reanimated.ScrollView, ScrollViewWithBottomPaddingProps >( ( { ScrollViewComponent, bottomPadding, scrollIndicatorPadding, contentInset, scrollIndicatorInsets, inverted, contentOffsetY, applyWorkaroundForContentInsetHitTestBug, children, ...rest }, ref, ) => { const prevContentOffsetY = useSharedValue(null); const animatedProps = useAnimatedProps(() => { const insetTop = inverted ? bottomPadding.value : 0; const insetBottom = !inverted ? bottomPadding.value : 0; const bottom = insetBottom + (contentInset?.bottom || 0); const top = insetTop + (contentInset?.top || 0); const indicatorPadding = scrollIndicatorPadding ?? bottomPadding; const indicatorTop = (inverted ? indicatorPadding.value : 0) + (scrollIndicatorInsets?.top || 0); const indicatorBottom = (!inverted ? indicatorPadding.value : 0) + (scrollIndicatorInsets?.bottom || 0); const result: Record = { // iOS prop contentInset: { bottom: bottom, top: top, right: contentInset?.right, left: contentInset?.left, }, scrollIndicatorInsets: { bottom: indicatorBottom, top: indicatorTop, right: scrollIndicatorInsets?.right, left: scrollIndicatorInsets?.left, }, // Android prop contentInsetBottom: insetBottom, contentInsetTop: insetTop, }; if (contentOffsetY) { const curr = contentOffsetY.value; if (curr !== prevContentOffsetY.value) { // eslint-disable-next-line react-compiler/react-compiler prevContentOffsetY.value = curr; result.contentOffset = { x: 0, y: curr }; } } return result; }, [ contentInset?.bottom, contentInset?.top, contentInset?.right, contentInset?.left, scrollIndicatorInsets?.bottom, scrollIndicatorInsets?.top, scrollIndicatorInsets?.right, scrollIndicatorInsets?.left, inverted, contentOffsetY, ]); return ( {inverted ? ( // The only thing it can break is `StickyHeader`, but it's already broken in FlatList and other lists // don't support this functionality, so we can add additional view here // The correct fix would be to add a new prop in ScrollView that allows // to customize children extraction logic and skip custom view {children} ) : ( children )} ); }, ); export default ScrollViewWithBottomPadding;