@rific/scroll-view 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +319 -0
  2. package/dist/index.d.mts +210 -0
  3. package/dist/index.d.ts +210 -0
  4. package/dist/index.js +2760 -0
  5. package/dist/index.mjs +2709 -0
  6. package/package.json +105 -0
  7. package/src/CustomList.tsx +127 -0
  8. package/src/FlatList.tsx +139 -0
  9. package/src/PullSearch.tsx +99 -0
  10. package/src/ScrollView.tsx +89 -0
  11. package/src/ScrollViewContext.ts +50 -0
  12. package/src/ScrollViewFooter.tsx +48 -0
  13. package/src/ScrollViewHeader.tsx +159 -0
  14. package/src/ScrollViewProvider.tsx +62 -0
  15. package/src/ScrollViewSettingsContext.ts +27 -0
  16. package/src/ScrollViewSettingsProvider.tsx +31 -0
  17. package/src/SectionList.tsx +195 -0
  18. package/src/__mocks__/auto-paper.ts +8 -0
  19. package/src/__mocks__/flash-list.ts +20 -0
  20. package/src/__mocks__/react-native-gesture-handler.ts +13 -0
  21. package/src/__mocks__/react-native-keyboard-controller.ts +11 -0
  22. package/src/__mocks__/react-native-paper.ts +22 -0
  23. package/src/__mocks__/react-native-reanimated.ts +30 -0
  24. package/src/__mocks__/react-native-safe-area-context.ts +2 -0
  25. package/src/__mocks__/react-native.ts +30 -0
  26. package/src/__tests__/CustomList.test.tsx +61 -0
  27. package/src/__tests__/FlatList.test.tsx +52 -0
  28. package/src/__tests__/PullSearch.test.tsx +38 -0
  29. package/src/__tests__/ScrollView.test.tsx +23 -0
  30. package/src/__tests__/ScrollViewHeader.test.tsx +28 -0
  31. package/src/__tests__/ScrollViewProvider.test.tsx +65 -0
  32. package/src/__tests__/SectionList.test.tsx +88 -0
  33. package/src/index.ts +26 -0
  34. package/src/internal/FAB.tsx +6 -0
  35. package/src/internal/HorizontalDots.tsx +99 -0
  36. package/src/internal/RefreshControl.tsx +5 -0
  37. package/src/internal/ScrollViewChip.tsx +22 -0
  38. package/src/internal/useScrollHandler.ts +101 -0
  39. package/src/internal/useScrollList.ts +64 -0
  40. package/src/internal/useStickyHeaders.ts +149 -0
  41. package/src/out.txt +61 -0
  42. package/src/redux/scrollViewSlice.ts +14 -0
  43. package/src/useKeyboardInset.ts +21 -0
  44. package/src/useScrollInit.ts +112 -0
  45. package/src/useScrollView.ts +8 -0
  46. package/src/useScrollViewSettings.ts +5 -0
@@ -0,0 +1,64 @@
1
+ import { useContext, useEffect, useMemo } from 'react'
2
+ import { type StyleProp, StyleSheet, type ViewStyle } from 'react-native'
3
+ import { useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'
4
+ import { useSafeAreaInsets } from 'react-native-safe-area-context'
5
+
6
+ import { ScrollViewContext } from '../ScrollViewContext'
7
+ import { useKeyboardInset } from '../useKeyboardInset'
8
+
9
+ const CHIP_SLIDE = 48
10
+
11
+ export type UseScrollListOptions = {
12
+ footerFixed?: boolean
13
+ headerFixed?: boolean
14
+ isHorizontal?: boolean
15
+ keyboardAware?: boolean
16
+ pullSearchHeight?: number
17
+ style?: StyleProp<ViewStyle>
18
+ }
19
+
20
+ export function useScrollList({ footerFixed: footerFixedProp, headerFixed: headerFixedProp, isHorizontal, keyboardAware, pullSearchHeight, style }: UseScrollListOptions = {}) {
21
+ const insets = useSafeAreaInsets()
22
+ const keyboardHeight = useKeyboardInset()
23
+ const { footerHeight, footerFixed: contextFooterFixed, headerHeight, headerFixed: contextHeaderFixed, headerOffset, pullSearchHeightShared, scrollPosition, snapBackHeaderShared } = useContext(ScrollViewContext)
24
+
25
+ const headerFixed = isHorizontal ? true : (headerFixedProp ?? contextHeaderFixed)
26
+ const footerFixed = isHorizontal ? true : (footerFixedProp ?? contextFooterFixed)
27
+
28
+ useEffect(() => {
29
+ pullSearchHeightShared.value = pullSearchHeight ?? 0
30
+ }, [pullSearchHeight, pullSearchHeightShared])
31
+
32
+ // First render (headerHeight===0): flex:1 so the list sits naturally below the in-flow header.
33
+ // After header measures: absoluteFill with contentOffset to match the same visual position.
34
+ const containerStyle = useMemo(() => (headerHeight === 0 ? [{ flex: 1 }, style] : [StyleSheet.absoluteFill, style]), [headerHeight, style])
35
+
36
+ const contentInset = useMemo(() => ({ bottom: (footerFixed ? footerHeight || insets.bottom : insets.bottom) + (keyboardAware ? keyboardHeight : 0), top: headerHeight }), [footerFixed, footerHeight, insets.bottom, headerHeight, keyboardAware, keyboardHeight])
37
+
38
+ // Synchronously positions the scroll view to match the in-flow layout from the first render.
39
+ // Fabric applies this atomically with the containerStyle switch, so there is no visible jump.
40
+ const contentOffset = useMemo(() => ({ x: 0, y: -contentInset.top }), [contentInset.top])
41
+
42
+ const chipHidden = useSharedValue(1)
43
+ const chipStyle = useAnimatedStyle(() => {
44
+ const pointerEvents = chipHidden.value ? ('none' as const) : ('box-none' as const)
45
+ if (isHorizontal) {
46
+ return {
47
+ opacity: chipHidden.value ? withTiming(0) : withTiming(1),
48
+ pointerEvents,
49
+ top: Math.max(headerHeight, insets.top) + 4,
50
+ transform: [{ translateX: chipHidden.value ? withTiming(-CHIP_SLIDE) : withTiming(0) }]
51
+ }
52
+ }
53
+ const slide = headerFixed ? 0 : snapBackHeaderShared.value ? -headerOffset.value : Math.max(0, scrollPosition.value + headerHeight - pullSearchHeightShared.value)
54
+ const top = Math.max(headerHeight - slide, insets.top) + 4
55
+ return {
56
+ opacity: chipHidden.value ? withTiming(0) : withTiming(1),
57
+ pointerEvents,
58
+ top,
59
+ transform: [{ translateY: chipHidden.value ? withTiming(-CHIP_SLIDE) : withTiming(0) }]
60
+ }
61
+ }, [isHorizontal, headerFixed, headerHeight, insets.top])
62
+
63
+ return { chipHidden, chipStyle, containerStyle, contentInset, contentOffset, footerFixed, headerFixed, headerHeight, insets, isHorizontal: isHorizontal ?? false }
64
+ }
@@ -0,0 +1,149 @@
1
+ import { useCallback, useContext, useLayoutEffect, useRef, useState } from 'react'
2
+ import { runOnJS, useAnimatedReaction, useAnimatedStyle, useSharedValue } from 'react-native-reanimated'
3
+ import { useSafeAreaInsets } from 'react-native-safe-area-context'
4
+
5
+ import { ScrollViewContext } from '../ScrollViewContext'
6
+
7
+ export type StickyHeadersApi = {
8
+ activeIndex: number
9
+ clipStyle: ReturnType<typeof useAnimatedStyle>
10
+ measureHeader: (index: number, y: number, height: number) => void
11
+ overlayStyle: ReturnType<typeof useAnimatedStyle>
12
+ resetPositions: (count: number) => void
13
+ }
14
+
15
+ export function useStickyHeaders(headerFixed: boolean, enabled: boolean): StickyHeadersApi {
16
+ const { headerHeightShared, headerOffset, pullSearchHeightShared, scrollPosition, snapBackHeaderShared } = useContext(ScrollViewContext)
17
+ const insetsTop = useSafeAreaInsets().top
18
+
19
+ const sectionYPositions = useSharedValue<number[]>([])
20
+ const sectionHeights = useSharedValue<number[]>([])
21
+ const [activeIndex, setActiveIndex] = useState(-1)
22
+ // Mirrors activeIndex React state but lives on the UI thread for overlayStyle.
23
+ // Updated via useLayoutEffect so content and style always change together.
24
+ const contentIndexShared = useSharedValue(-1)
25
+ useLayoutEffect(() => {
26
+ contentIndexShared.value = activeIndex
27
+ }, [activeIndex, contentIndexShared])
28
+
29
+ // JS-side source of truth — sectionYPositions is a snapshot pushed to the worklet.
30
+ // Reading sectionYPositions.value from JS returns the UI-thread's stale copy, so
31
+ // rapid async measure callbacks would overwrite each other; refs avoid that.
32
+ const positionsCache = useRef<number[]>([])
33
+ const heightsCache = useRef<number[]>([])
34
+
35
+ useAnimatedReaction(
36
+ () => {
37
+ if (!enabled) return -1
38
+ const positions = sectionYPositions.value
39
+ if (positions.length === 0) return -1
40
+
41
+ const hHeight = headerHeightShared.value
42
+ const scrollPos = scrollPosition.value
43
+ let stickyTop: number
44
+ if (headerFixed) {
45
+ stickyTop = hHeight
46
+ } else if (snapBackHeaderShared.value) {
47
+ stickyTop = Math.max(insetsTop, hHeight + headerOffset.value)
48
+ } else {
49
+ const effective = scrollPos + hHeight - pullSearchHeightShared.value
50
+ stickyTop = effective <= 0 ? hHeight : Math.max(insetsTop, hHeight - effective)
51
+ }
52
+
53
+ const threshold = scrollPos + stickyTop
54
+ let active = -1
55
+ for (let i = 0; i < positions.length; i++) {
56
+ if (positions[i] >= 0 && positions[i] < threshold) active = i
57
+ else if (positions[i] >= 0) break
58
+ }
59
+
60
+ return active
61
+ },
62
+ (curr, prev) => {
63
+ if (curr !== prev) runOnJS(setActiveIndex)(curr)
64
+ },
65
+ [enabled, headerFixed, insetsTop]
66
+ )
67
+
68
+ // Positions the clip container at stickyTop so overflow:hidden hides the outgoing
69
+ // header as it slides above the sticky line during a push transition.
70
+ const clipStyle = useAnimatedStyle(() => {
71
+ const hHeight = headerHeightShared.value
72
+ const scrollPos = scrollPosition.value
73
+ let stickyTop: number
74
+ if (!enabled || headerFixed) {
75
+ stickyTop = hHeight
76
+ } else if (snapBackHeaderShared.value) {
77
+ stickyTop = Math.max(insetsTop, hHeight + headerOffset.value)
78
+ } else {
79
+ const effective = scrollPos + hHeight - pullSearchHeightShared.value
80
+ stickyTop = effective <= 0 ? hHeight : Math.max(insetsTop, hHeight - effective)
81
+ }
82
+ return { top: stickyTop }
83
+ }, [enabled, headerFixed, insetsTop])
84
+
85
+ // overlayStyle positions the header at top:0 relative to the clip container.
86
+ // The push animation uses 1:1 speed with pushThreshold = stickyTop + currentHeight
87
+ // (incoming header touches outgoing header's bottom). The clip container's
88
+ // overflow:hidden hides the outgoing header once it exits the top of the container.
89
+ const overlayStyle = useAnimatedStyle(() => {
90
+ const hidden = { opacity: 0, top: 0, transform: [{ translateY: 0 }] }
91
+ if (!enabled) return hidden
92
+
93
+ const index = contentIndexShared.value
94
+ const positions = sectionYPositions.value
95
+ if (index < 0 || index >= positions.length) return hidden
96
+
97
+ const hHeight = headerHeightShared.value
98
+ const scrollPos = scrollPosition.value
99
+ let stickyTop: number
100
+ if (headerFixed) {
101
+ stickyTop = hHeight
102
+ } else if (snapBackHeaderShared.value) {
103
+ stickyTop = Math.max(insetsTop, hHeight + headerOffset.value)
104
+ } else {
105
+ const effective = scrollPos + hHeight - pullSearchHeightShared.value
106
+ stickyTop = effective <= 0 ? hHeight : Math.max(insetsTop, hHeight - effective)
107
+ }
108
+
109
+ if (positions[index] - scrollPos >= stickyTop) return hidden
110
+
111
+ const currentHeight = Math.max(0, sectionHeights.value[index] ?? 0)
112
+ const nextIndex = index + 1
113
+ let translateY = 0
114
+ if (currentHeight > 0 && nextIndex < positions.length && positions[nextIndex] >= 0) {
115
+ const nextScreenY = positions[nextIndex] - scrollPos
116
+ const pushThreshold = stickyTop + currentHeight
117
+ if (nextScreenY < pushThreshold) translateY = nextScreenY - pushThreshold
118
+ }
119
+
120
+ return { opacity: 1, top: 0, transform: [{ translateY }] }
121
+ }, [enabled, headerFixed, insetsTop])
122
+
123
+ // pageY is the view's window-coordinate Y from view.measure().
124
+ // contentY = pageY + contentOffset.y, since screenY = contentY - contentOffset.y.
125
+ const measureHeader = useCallback(
126
+ (index: number, pageY: number, height: number) => {
127
+ const contentY = pageY + scrollPosition.value
128
+ positionsCache.current[index] = contentY
129
+ sectionYPositions.value = positionsCache.current.slice()
130
+
131
+ heightsCache.current[index] = height
132
+ sectionHeights.value = heightsCache.current.slice()
133
+ },
134
+ [scrollPosition, sectionHeights, sectionYPositions]
135
+ )
136
+
137
+ const resetPositions = useCallback(
138
+ (count: number) => {
139
+ positionsCache.current = Array.from({ length: count }, () => -1)
140
+ sectionYPositions.value = positionsCache.current.slice()
141
+
142
+ heightsCache.current = Array.from({ length: count }, () => -1)
143
+ sectionHeights.value = heightsCache.current.slice()
144
+ },
145
+ [sectionHeights, sectionYPositions]
146
+ )
147
+
148
+ return { activeIndex, clipStyle, measureHeader, overlayStyle, resetPositions }
149
+ }
package/src/out.txt ADDED
@@ -0,0 +1,61 @@
1
+ jay~/Developer/React-Native-Scroll-View% yalc publish && yalc push
2
+ Running prepublishOnly script: npm run build
3
+
4
+ > @rific/scroll-view@0.1.0 prepublishOnly
5
+ > npm run build
6
+
7
+
8
+ > @rific/scroll-view@0.1.0 build
9
+ > tsup src/index.ts --format cjs,esm --dts --clean
10
+
11
+ CLI Building entry: src/index.ts
12
+ CLI Using tsconfig: tsconfig.json
13
+ CLI tsup v8.5.1
14
+ CLI Target: es2020
15
+ CLI Cleaning output folder
16
+ CJS Build start
17
+ ESM Build start
18
+ CJS dist/index.js 103.45 KB
19
+ CJS ⚡️ Build success in 78ms
20
+ ESM dist/index.mjs 95.24 KB
21
+ ESM ⚡️ Build success in 78ms
22
+ DTS Build start
23
+ src/ScrollView.tsx(48,49): error TS2339: Property 'scrollTo' does not exist on type 'ScrollResponderMixin'.
24
+
25
+ Error: error occurred in dts build
26
+ at Worker.<anonymous> (/Users/jay/Developer/React-Native-Scroll-View/node_modules/tsup/dist/index.js:1545:26)
27
+ at Worker.emit (node:events:509:20)
28
+ at MessagePort.<anonymous> (node:internal/worker:333:53)
29
+ at [nodejs.internal.kHybridDispatch] (node:internal/event_target:843:20)
30
+ at MessagePort.<anonymous> (node:internal/per_context/messageport:23:28)
31
+ DTS Build error
32
+ yalc publish
33
+
34
+ Publish package in yalc local repo
35
+
36
+ Options:
37
+ --version Show version number [boolean]
38
+ --help Show help [boolean]
39
+ --sig [boolean] [default: false]
40
+ --dev-mod [boolean] [default: true]
41
+ --workspace-resolve [default: true]
42
+
43
+ Error: Command failed: npm run prepublishOnly
44
+ at genericNodeError (node:internal/errors:998:15)
45
+ at wrappedFn (node:internal/errors:543:14)
46
+ at checkExecSyncError (node:child_process:925:11)
47
+ at Object.execSync (node:child_process:997:15)
48
+ at runPmScript (/opt/homebrew/lib/node_modules/yalc/src/publish.js:75:41)
49
+ at Array.forEach (<anonymous>)
50
+ at /opt/homebrew/lib/node_modules/yalc/src/publish.js:90:28
51
+ at step (/opt/homebrew/lib/node_modules/yalc/src/publish.js:44:23)
52
+ at Object.next (/opt/homebrew/lib/node_modules/yalc/src/publish.js:25:53)
53
+ at /opt/homebrew/lib/node_modules/yalc/src/publish.js:19:71 {
54
+ status: 1,
55
+ signal: null,
56
+ output: [ null, null, null ],
57
+ pid: 71344,
58
+ stdout: null,
59
+ stderr: null
60
+ }
61
+ jay~/Developer/React-Native-Scroll-View%
@@ -0,0 +1,14 @@
1
+ import { createSlice, type PayloadAction } from '@reduxjs/toolkit'
2
+
3
+ import { defaultScrollViewSettings, type ScrollViewSettings } from '../ScrollViewSettingsContext'
4
+
5
+ const slice = createSlice({
6
+ name: 'scrollView',
7
+ initialState: defaultScrollViewSettings,
8
+ reducers: {
9
+ initialize: (_state, action: PayloadAction<ScrollViewSettings>) => action.payload
10
+ }
11
+ })
12
+
13
+ export const scrollViewActions = slice.actions
14
+ export const scrollViewReducer = slice.reducer
@@ -0,0 +1,21 @@
1
+ import { useState } from 'react'
2
+ import { useKeyboardHandler } from 'react-native-keyboard-controller'
3
+ import { runOnJS } from 'react-native-reanimated'
4
+
5
+ export const useKeyboardInset = () => {
6
+ const [keyboardHeight, setKeyboardHeight] = useState(0)
7
+ useKeyboardHandler(
8
+ {
9
+ onMove: (e) => {
10
+ 'worklet'
11
+ runOnJS(setKeyboardHeight)(e.height)
12
+ },
13
+ onEnd: (e) => {
14
+ 'worklet'
15
+ runOnJS(setKeyboardHeight)(e.height)
16
+ }
17
+ },
18
+ []
19
+ )
20
+ return keyboardHeight
21
+ }
@@ -0,0 +1,112 @@
1
+ import { type ComponentType, createElement, type ReactElement, useCallback, useContext, useEffect, useLayoutEffect, useRef, useState } from 'react'
2
+ import type { NativeScrollEvent, NativeSyntheticEvent } from 'react-native'
3
+
4
+ import { ScrollViewContext } from './ScrollViewContext'
5
+
6
+ export type UseScrollInitOptions = {
7
+ listHeaderComponent?: ComponentType<object> | ReactElement | null
8
+ onMomentumScrollEnd?: (e: NativeSyntheticEvent<NativeScrollEvent>) => void
9
+ onRefresh?: () => Promise<void> | void
10
+ onScrollBeginDrag?: (e: NativeSyntheticEvent<NativeScrollEvent>) => void
11
+ onScrollEndDrag?: (e: NativeSyntheticEvent<NativeScrollEvent>) => void
12
+ pullSearchHeight?: number
13
+ scrollTo: (offset: number, animated: boolean) => void
14
+ }
15
+
16
+ export type UseScrollInitResult = {
17
+ activeListHeader: ComponentType<object> | ReactElement | null | undefined
18
+ handleMomentumScrollEnd: (e: NativeSyntheticEvent<NativeScrollEvent>) => void
19
+ handleScrollBeginDrag: (e: NativeSyntheticEvent<NativeScrollEvent>) => void
20
+ handleScrollEndDrag: (e: NativeSyntheticEvent<NativeScrollEvent>) => void
21
+ hiddenHeader: ReactElement | null
22
+ pullSearchMinHeight: number
23
+ }
24
+
25
+ export function useScrollInit({ listHeaderComponent, onMomentumScrollEnd: externalMomentumScrollEnd, onRefresh, onScrollBeginDrag: externalScrollBeginDrag, onScrollEndDrag: externalScrollEndDrag, pullSearchHeight, scrollTo }: UseScrollInitOptions): UseScrollInitResult {
26
+ const { headerHeight, scrollPosition, setProgressing } = useContext(ScrollViewContext)
27
+ const hasPullSearch = pullSearchHeight !== undefined
28
+ const [phase, setPhase] = useState<'measuring' | 'ready'>(hasPullSearch ? 'measuring' : 'ready')
29
+ const [refreshing, setRefreshing] = useState(false)
30
+ const dragStartY = useRef<number | null>(null)
31
+ const deliberatelyOpened = useRef(false)
32
+
33
+ // Transition to 'ready' once both measurements are available
34
+ useEffect(() => {
35
+ if (phase === 'ready' || !headerHeight || !pullSearchHeight) return
36
+ scrollPosition.value = -headerHeight + pullSearchHeight
37
+ setPhase('ready')
38
+ }, [phase, headerHeight, pullSearchHeight, scrollPosition])
39
+
40
+ // Effect A: scroll to natural top the first time headerHeight is measured
41
+ const headerInitialized = useRef(false)
42
+ useLayoutEffect(() => {
43
+ if (!headerHeight || headerInitialized.current) return
44
+ headerInitialized.current = true
45
+ scrollPosition.value = -headerHeight
46
+ scrollTo(-headerHeight, false)
47
+ }, [headerHeight, scrollPosition, scrollTo])
48
+
49
+ // Effect B: scroll to hideY when pullSearch phase is ready (same Fabric commit as ListHeaderComponent addition)
50
+ const pullSearchInitialized = useRef(false)
51
+ useLayoutEffect(() => {
52
+ if (!hasPullSearch || phase !== 'ready' || pullSearchInitialized.current || !headerHeight || !pullSearchHeight) return
53
+ pullSearchInitialized.current = true
54
+ scrollPosition.value = -headerHeight + pullSearchHeight
55
+ scrollTo(-headerHeight + pullSearchHeight, false)
56
+ }, [hasPullSearch, phase, headerHeight, pullSearchHeight, scrollPosition, scrollTo])
57
+
58
+ useEffect(() => {
59
+ if (pullSearchHeight) setProgressing(refreshing)
60
+ }, [pullSearchHeight, refreshing, setProgressing])
61
+
62
+ const handleScrollBeginDrag = useCallback(
63
+ (e: NativeSyntheticEvent<NativeScrollEvent>) => {
64
+ deliberatelyOpened.current = false
65
+ if (pullSearchHeight) dragStartY.current = e.nativeEvent.contentOffset.y
66
+ externalScrollBeginDrag?.(e)
67
+ },
68
+ [externalScrollBeginDrag, pullSearchHeight]
69
+ )
70
+
71
+ const handleScrollEndDrag = useCallback(
72
+ (e: NativeSyntheticEvent<NativeScrollEvent>) => {
73
+ if (pullSearchHeight && headerHeight) {
74
+ const y = e.nativeEvent.contentOffset.y
75
+ const showY = -headerHeight
76
+ const hideY = -headerHeight + pullSearchHeight
77
+ const pulledDown = (dragStartY.current ?? hideY) <= hideY
78
+ dragStartY.current = null
79
+ if (pulledDown && y <= showY + pullSearchHeight * 0.5) {
80
+ deliberatelyOpened.current = true
81
+ if (y <= showY && !refreshing && onRefresh) {
82
+ setRefreshing(true)
83
+ Promise.resolve(onRefresh()).finally(() => setRefreshing(false))
84
+ }
85
+ scrollTo(showY, true)
86
+ } else if (y < hideY) {
87
+ scrollTo(hideY, true)
88
+ }
89
+ }
90
+ externalScrollEndDrag?.(e)
91
+ },
92
+ [externalScrollEndDrag, headerHeight, onRefresh, pullSearchHeight, refreshing, scrollTo]
93
+ )
94
+
95
+ const handleMomentumScrollEnd = useCallback(
96
+ (e: NativeSyntheticEvent<NativeScrollEvent>) => {
97
+ if (pullSearchHeight && headerHeight && !deliberatelyOpened.current && !refreshing) {
98
+ const y = e.nativeEvent.contentOffset.y
99
+ const hideY = -headerHeight + pullSearchHeight
100
+ if (y < hideY - 1) scrollTo(hideY, true)
101
+ }
102
+ deliberatelyOpened.current = false
103
+ externalMomentumScrollEnd?.(e)
104
+ },
105
+ [externalMomentumScrollEnd, headerHeight, pullSearchHeight, refreshing, scrollTo]
106
+ )
107
+
108
+ const activeListHeader = hasPullSearch && phase === 'measuring' ? null : listHeaderComponent
109
+ const hiddenHeader = phase === 'measuring' && listHeaderComponent != null ? (typeof listHeaderComponent === 'function' ? createElement(listHeaderComponent as ComponentType<object>) : (listHeaderComponent as ReactElement)) : null
110
+
111
+ return { activeListHeader, handleMomentumScrollEnd, handleScrollBeginDrag, handleScrollEndDrag, hiddenHeader, pullSearchMinHeight: pullSearchHeight ?? 0 }
112
+ }
@@ -0,0 +1,8 @@
1
+ import { useContext } from 'react'
2
+
3
+ import { ScrollViewContext } from './ScrollViewContext'
4
+
5
+ export const useScrollView = () => {
6
+ const { progress, progressing, scrollHeight, scrollPosition, setProgress, setProgressing } = useContext(ScrollViewContext)
7
+ return { progress, progressing, scrollHeight, scrollPosition, setProgress, setProgressing }
8
+ }
@@ -0,0 +1,5 @@
1
+ import { useContext } from 'react'
2
+
3
+ import { ScrollViewSettingsContext } from './ScrollViewSettingsContext'
4
+
5
+ export const useScrollViewSettings = () => useContext(ScrollViewSettingsContext)