@attio/react-native-bottom-sheet-toolbox
Version:
102 lines (86 loc) • 3.5 kB
text/typescript
import type {SectionList} from "react-native"
import type Animated from "react-native-reanimated"
import {type FlatList, Gesture, type ScrollView} from "react-native-gesture-handler"
import {
type AnimatedRef,
type SharedValue,
scrollTo,
useDerivedValue,
useSharedValue,
} from "react-native-reanimated"
import {useBottomSheetToolboxInternalContext} from "../context"
import {useBottomSheetDragGesture} from "../hooks/use-bottom-sheet-drag-gesture"
export type Scrollable<T> =
| FlatList<T>
| ScrollView
| SectionList<T>
| Animated.FlatList<T>
| Animated.ScrollView
// biome-ignore lint/suspicious/noExplicitAny: not useful to type
| any
export interface UseBottomSheetScrollGestureArgs<T> {
scrollRef: AnimatedRef<Scrollable<T>>
scrollOffsetYSharedValue: SharedValue<number>
scrollContentHeightSharedValue: SharedValue<number | null>
scrollLayoutHeightSharedValue: SharedValue<number | null>
inverted: boolean
isFrozenSharedValue?: SharedValue<boolean>
}
export function useBottomSheetScrollGesture<T>({
scrollRef,
scrollOffsetYSharedValue,
scrollContentHeightSharedValue,
scrollLayoutHeightSharedValue,
inverted,
isFrozenSharedValue,
}: UseBottomSheetScrollGestureArgs<T>) {
const {normalizedSnapPointsSharedValue, currentPositionSharedValue} =
useBottomSheetToolboxInternalContext()
const isExpandedSharedValue = useDerivedValue(() => {
const maxSnap = Math.max(...(normalizedSnapPointsSharedValue.get() || []))
return currentPositionSharedValue.get() === maxSnap
}, [normalizedSnapPointsSharedValue, currentPositionSharedValue])
const panFrozenSharedValue = useDerivedValue(() => {
let isAtTop = false
const scrollOffsetY = scrollOffsetYSharedValue.get()
if (inverted) {
const scrollLayoutHeight = scrollLayoutHeightSharedValue.get()
const scrollContentHeight = scrollContentHeightSharedValue.get()
const isReady = scrollLayoutHeight !== null && scrollContentHeight !== null
const ROUNDING_ERROR_EPSILON = 0.1
isAtTop =
isReady &&
scrollContentHeight - scrollOffsetY - scrollLayoutHeight <= ROUNDING_ERROR_EPSILON
} else {
isAtTop = scrollOffsetY <= 0
}
return isFrozenSharedValue?.get() === true || (isExpandedSharedValue.get() && !isAtTop)
}, [
scrollOffsetYSharedValue,
isFrozenSharedValue,
inverted,
scrollContentHeightSharedValue.get,
scrollLayoutHeightSharedValue.get,
])
const panGesture = useBottomSheetDragGesture({
id: "scroll",
clamped: true,
frozenSharedValue: panFrozenSharedValue,
})
const nativeGesture = Gesture.Native()
const initialScrollOffsetYSharedValue = useSharedValue(0)
const scrollControllerGesture = Gesture.Pan()
.onBegin(() => {
initialScrollOffsetYSharedValue.set(scrollOffsetYSharedValue.get())
})
.onUpdate(() => {
if (!isExpandedSharedValue.get()) {
scrollTo(scrollRef, 0, initialScrollOffsetYSharedValue.get(), false)
} else {
initialScrollOffsetYSharedValue.set(scrollOffsetYSharedValue.get())
}
})
.failOffsetY(9999999999)
.failOffsetX(9999999999)
return Gesture.Simultaneous(nativeGesture, panGesture, scrollControllerGesture)
}