@rific/scroll-view 0.8.3 → 0.9.1

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.
@@ -26,7 +26,7 @@ export function useStickyHeaders(headerFixed: boolean, enabled: boolean): Sticky
26
26
  contentIndexShared.value = activeIndex
27
27
  }, [activeIndex, contentIndexShared])
28
28
 
29
- // JS-side source of truth — sectionYPositions is a snapshot pushed to the worklet.
29
+ // JS-side source of truth - sectionYPositions is a snapshot pushed to the worklet.
30
30
  // Reading sectionYPositions.value from JS returns the UI-thread's stale copy, so
31
31
  // rapid async measure callbacks would overwrite each other; refs avoid that.
32
32
  const positionsCache = useRef<number[]>([])
@@ -65,7 +65,7 @@ export function useStickyHeaders(headerFixed: boolean, enabled: boolean): Sticky
65
65
  // Same web-only reanimated gap as useAnimatedStyle elsewhere in this file: this "prepare"
66
66
  // function's own SharedValue reads (sectionYPositions, headerHeightShared, scrollPosition,
67
67
  // snapBackHeaderShared, headerOffset, pullSearchHeightShared) need to be listed explicitly too
68
- // — confirmed empirically that without them, useAnimatedReaction re-evaluates once on mount and
68
+ // - confirmed empirically that without them, useAnimatedReaction re-evaluates once on mount and
69
69
  // then never again on web, permanently freezing activeIndex/the active sticky section.
70
70
  [enabled, headerFixed, insetsTop, sectionYPositions, headerHeightShared, scrollPosition, snapBackHeaderShared, headerOffset, pullSearchHeightShared]
71
71
  )
@@ -74,7 +74,7 @@ export function useStickyHeaders(headerFixed: boolean, enabled: boolean): Sticky
74
74
  // header as it slides above the sticky line during a push transition.
75
75
  // Every SharedValue read below (headerHeightShared, scrollPosition, snapBackHeaderShared,
76
76
  // headerOffset, pullSearchHeightShared, and for overlayStyle also contentIndexShared/
77
- // sectionYPositions/sectionHeights) has to be listed in the array too — see
77
+ // sectionYPositions/sectionHeights) has to be listed in the array too - see
78
78
  // ScrollViewHeader's translateStyle for why: on web, reanimated doesn't treat mutations to a
79
79
  // SharedValue as a reactive trigger for useAnimatedStyle unless it's in this array.
80
80
  const clipStyle = useAnimatedStyle(() => {
@@ -2,7 +2,7 @@ import { createSettingsSlice } from '@rific/core'
2
2
 
3
3
  import { defaultScrollViewSettings, type ScrollViewSettings } from '../ScrollViewSettingsContext'
4
4
 
5
- // scrollView has no per-field setters, just a full-replace initialize — matches the previous
5
+ // scrollView has no per-field setters, just a full-replace initialize - matches the previous
6
6
  // hand-rolled slice exactly (action type 'scrollView/initialize', full-state-replace behavior).
7
7
  const scrollViewSlice = createSettingsSlice<ScrollViewSettings>('scrollView', {
8
8
  initialState: defaultScrollViewSettings,
@@ -23,6 +23,6 @@ const useKeyboardInsetNative = () => {
23
23
 
24
24
  const useKeyboardInsetWeb = () => 0
25
25
 
26
- // react-native-keyboard-controller has no web implementation — its handler hook must never be
26
+ // react-native-keyboard-controller has no web implementation - its handler hook must never be
27
27
  // called there. Platform is constant for the life of the bundle, so this select is hook-safe.
28
28
  export const useKeyboardInset = Platform.OS === 'web' ? useKeyboardInsetWeb : useKeyboardInsetNative
@@ -12,7 +12,7 @@ const REMOUNT_PRIME_FRAMES = 40
12
12
  const SNAP_VERIFY_TOLERANCE = 1
13
13
  const SNAP_VERIFY_MAX_ATTEMPTS = 5
14
14
  // Long enough to comfortably outlast RN's default animated scrollTo duration (iOS's native
15
- // setContentOffset:animated: for a typical pull-search distance settles well within this) — see
15
+ // setContentOffset:animated: for a typical pull-search distance settles well within this) - see
16
16
  // the comment on scheduleSnapVerification for why a native onMomentumScrollEnd event can't be used
17
17
  // as this signal instead.
18
18
  const SNAP_VERIFY_INITIAL_DELAY_MS = 400
@@ -51,7 +51,7 @@ export function useScrollInit({ listHeaderComponent, onMomentumScrollEnd: extern
51
51
  // switch). Only enter 'measuring' on the very first mount when headerHeight hasn't been set yet.
52
52
  const [phase, setPhase] = useState<'measuring' | 'ready'>(hasPullSearch && !headerHeight ? 'measuring' : 'ready')
53
53
  const [refreshing, setRefreshing] = useState(false)
54
- // A plain-ref mirror of `refreshing`, read from inside the deferred timeout below — that
54
+ // A plain-ref mirror of `refreshing`, read from inside the deferred timeout below - that
55
55
  // callback can fire well after the render that scheduled it, so it needs the *current* value,
56
56
  // not whatever `refreshing` closed over at schedule time.
57
57
  const refreshingRef = useRef(refreshing)
@@ -60,7 +60,7 @@ export function useScrollInit({ listHeaderComponent, onMomentumScrollEnd: extern
60
60
  }, [refreshing])
61
61
  const dragStartY = useRef<number | null>(null)
62
62
  // Which pull-search resting position (showY or hideY) a drag-release most recently committed
63
- // to, so a late fallback in handleMomentumScrollEnd knows not to fight it — see
63
+ // to, so a late fallback in handleMomentumScrollEnd knows not to fight it - see
64
64
  // scheduleSnapVerification below for how this actually gets enforced.
65
65
  const pendingSnapTarget = useRef<number | null>(null)
66
66
  const snapVerifyTimeout = useRef<ReturnType<typeof setTimeout> | null>(null)
@@ -69,9 +69,9 @@ export function useScrollInit({ listHeaderComponent, onMomentumScrollEnd: extern
69
69
  // Verifies a pull-search drag-release (handleScrollEndDrag) actually landed on `target`, and
70
70
  // keeps correcting (non-animated, so it lands immediately) until it does. This is deliberately
71
71
  // time-based rather than triggered by onMomentumScrollEnd: a slow/controlled release with near-
72
- // zero velocity fires onMomentumScrollEnd within a couple of milliseconds of the drag ending —
72
+ // zero velocity fires onMomentumScrollEnd within a couple of milliseconds of the drag ending -
73
73
  // long before the *animated* scrollTo call issued moments earlier has had any chance to actually
74
- // move — so reacting to that event by immediately snapping (non-animated) to the target replaced
74
+ // move - so reacting to that event by immediately snapping (non-animated) to the target replaced
75
75
  // the intended smooth spring-back animation with a jarring instant jump on every normal pull,
76
76
  // not just the pathological case this was built for. Waiting out a fixed delay long enough for a
77
77
  // normal animated scrollTo to have finished, then checking the *actual* position via
@@ -111,7 +111,7 @@ export function useScrollInit({ listHeaderComponent, onMomentumScrollEnd: extern
111
111
  []
112
112
  )
113
113
 
114
- // Remount sync: this instance mounted with headerHeight already known — a key-forced remount
114
+ // Remount sync: this instance mounted with headerHeight already known - a key-forced remount
115
115
  // (e.g. numColumns toggling between a list and a grid), not a true first mount. The native
116
116
  // scroll view's actual position right after such a remount isn't reliable (Fabric's ScrollView
117
117
  // recycling and Reanimated's Animated.FlatList can both leave it at a stale or zeroed offset
@@ -159,7 +159,7 @@ export function useScrollInit({ listHeaderComponent, onMomentumScrollEnd: extern
159
159
  }, REMOUNT_SYNC_FALLBACK_MS)
160
160
  return () => clearTimeout(fallback)
161
161
  // Intentionally runs once per mount only, using whatever remountTarget was captured at
162
- // construction — not a dep that should retrigger this effect.
162
+ // construction - not a dep that should retrigger this effect.
163
163
  // eslint-disable-next-line react-hooks/exhaustive-deps
164
164
  }, [])
165
165
 
@@ -170,10 +170,10 @@ export function useScrollInit({ listHeaderComponent, onMomentumScrollEnd: extern
170
170
  setPhase('ready')
171
171
  }, [phase, headerHeight, pullSearchHeight, scrollPosition])
172
172
 
173
- // Effect A applies ONLY on an actual remount (remountTarget !== null — this instance mounted with
173
+ // Effect A applies ONLY on an actual remount (remountTarget !== null - this instance mounted with
174
174
  // headerHeight already known, e.g. a key-forced remount elsewhere in the tree). It syncs
175
175
  // scrollPosition synchronously before first paint and issues a short bounded chain of imperative
176
- // scrollTo calls, alternating between two offsets far enough apart (30pt — comfortably more than
176
+ // scrollTo calls, alternating between two offsets far enough apart (30pt - comfortably more than
177
177
  // any plausible rounding/measurement noise) to guarantee at least one call can't be mistaken for a
178
178
  // no-op. The declarative `contentOffset` prop alone isn't enough on a remount: when the target is
179
179
  // numerically identical to what it was before (headerHeight/pullSearchHeight don't change across a
@@ -181,7 +181,7 @@ export function useScrollInit({ listHeaderComponent, onMomentumScrollEnd: extern
181
181
  // be left at whatever position it actually drifted to. Worse, iOS's imperative scrollToOffset
182
182
  // short-circuits with a CGPointEqualToPoint check against the native view's *actual* current
183
183
  // contentOffset before it will fire the native scroll-finished callback that RN turns into an
184
- // onScroll event — so a single differing call can still go unnoticed if it happens to race content
184
+ // onScroll event - so a single differing call can still go unnoticed if it happens to race content
185
185
  // that's still growing. Alternating across the *entire* correction window, not just the first
186
186
  // couple of frames, forces a real scroll-finished callback so drift becomes observable and the
187
187
  // onScroll-driven retry loop in useScrollHandlerJS can correct it.
@@ -189,7 +189,7 @@ export function useScrollInit({ listHeaderComponent, onMomentumScrollEnd: extern
189
189
  // On a true first mount (remountTarget === null) this effect does nothing beyond a single
190
190
  // insurance call against the destination view silently ignoring the very first scroll command
191
191
  // because it isn't fully installed in the window hierarchy yet (e.g. mid native-stack push
192
- // transition) — a fresh, non-recycled native view reliably honors the declarative contentOffset
192
+ // transition) - a fresh, non-recycled native view reliably honors the declarative contentOffset
193
193
  // prop otherwise. Repeating or oscillating that call here fights Effect B below, whose entire job
194
194
  // on a true first mount with pull-search is to push the position on from here to hideY: if this
195
195
  // effect keeps re-asserting its own target for tens of frames, it can win the race and leave the
@@ -277,7 +277,7 @@ export function useScrollInit({ listHeaderComponent, onMomentumScrollEnd: extern
277
277
  if (pullSearchHeight && headerHeight && !refreshing && pendingSnapTarget.current === null) {
278
278
  // No drag committed to a target above (e.g. a fast fling whose whole reveal/overscroll
279
279
  // motion happened during momentum, never during the drag itself, so handleScrollEndDrag
280
- // never saw a y near the pull-search zone) — fall back to auto-closing if we've ended up
280
+ // never saw a y near the pull-search zone) - fall back to auto-closing if we've ended up
281
281
  // revealed without ever meaning to.
282
282
  const y = e.nativeEvent.contentOffset.y
283
283
  const hideY = -headerHeight + pullSearchHeight
@@ -0,0 +1,41 @@
1
+ import { useContext, useEffect } from 'react'
2
+ import type { StyleProp, ViewStyle } from 'react-native'
3
+ import { type AnimatedStyle, useAnimatedStyle, useSharedValue } from 'react-native-reanimated'
4
+
5
+ import { ScrollViewChromeContext } from './ScrollViewChromeContext'
6
+
7
+ const noop = () => {}
8
+
9
+ // Animated style for a persistent bar hosted by ScrollViewChromeProvider: translates it by the same
10
+ // shared offset the focused screen's footer uses, so the two hide and reveal together. Outside any
11
+ // host it is a permanent zero translate, so a bar can call this unconditionally.
12
+ //
13
+ // The return type is spelled out with reanimated's PUBLIC AnimatedStyle rather than left inferred:
14
+ // useAnimatedStyle's own inferred type (AnimatedStyleHandle) lives in a private reanimated deep path
15
+ // that the built .d.ts would otherwise import from, which only resolves for the exact reanimated
16
+ // file layout this was built against.
17
+ export const useScrollViewChromeStyle = (): StyleProp<AnimatedStyle<StyleProp<ViewStyle>>> => {
18
+ const chrome = useContext(ScrollViewChromeContext)
19
+ const fallback = useSharedValue(0)
20
+ const offset = chrome?.offset ?? fallback
21
+ return useAnimatedStyle(() => ({ transform: [{ translateY: offset.value }] }), [offset])
22
+ }
23
+
24
+ // Holds the bar fully revealed (and stops scroll from hiding it) while `active` is true. Pins are
25
+ // counted, so two independent pinners can't release each other. Inert outside a host.
26
+ export const useScrollViewChromePin = (active: boolean) => {
27
+ const chrome = useContext(ScrollViewChromeContext)
28
+ const addPin = chrome?.addPin
29
+ const removePin = chrome?.removePin
30
+ useEffect(() => {
31
+ if (!active || !addPin || !removePin) return undefined
32
+ addPin()
33
+ return removePin
34
+ }, [active, addPin, removePin])
35
+ }
36
+
37
+ // Returns a function that animates the bar back to fully revealed. Inert outside a host.
38
+ export const useScrollViewChromeReveal = () => {
39
+ const chrome = useContext(ScrollViewChromeContext)
40
+ return chrome?.reveal ?? noop
41
+ }