@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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jay Deaton
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -14,7 +14,7 @@ npm install @rific/scroll-view
14
14
  npm install @rific/auto-paper react-native-reanimated react-native-gesture-handler react-native-safe-area-context react-native-paper react-native-keyboard-controller
15
15
  ```
16
16
 
17
- The exported Redux slice has no dependency on `@reduxjs/toolkit` — it works with RTK stores, vanilla Redux, or no Redux at all.
17
+ The exported Redux slice has no dependency on `@reduxjs/toolkit` - it works with RTK stores, vanilla Redux, or no Redux at all.
18
18
 
19
19
  ## Quick start
20
20
 
@@ -57,7 +57,7 @@ App-wide defaults for scroll settings. Wrap your root layout to set global defau
57
57
 
58
58
  | Prop | Type | Description |
59
59
  |------|------|-------------|
60
- | `children` | `ReactNode` | — |
60
+ | `children` | `ReactNode` | - |
61
61
  | `initialValue` | `Partial<ScrollViewSettings>` | Initial settings applied once on mount |
62
62
  | `onChange` | `(settings: ScrollViewSettings) => void` | Called whenever settings change |
63
63
 
@@ -70,14 +70,31 @@ Screen-level provider that owns header/footer state and scroll position. Must wr
70
70
  | Prop | Type | Default | Description |
71
71
  |------|------|---------|-------------|
72
72
  | `blur` | `boolean` | system | Enable frosted-glass backdrop on header/footer |
73
+ | `chrome` | `boolean` | `true` | Join an enclosing [`ScrollViewChromeProvider`](#scrollviewchromeprovider) when there is one. Pass `false` to opt out - for a nested provider that isn't a screen, such as a picker or form modal. No effect when nothing hosts |
73
74
  | `fixed` | `boolean` | `false` | Pin both header and footer (overrides `headerFixed`/`footerFixed`) |
74
- | `footerAboveKeyboard` | `boolean` | `false` | With a fixed footer, float it above the keyboard instead of letting the keyboard cover it — see [Keyboard awareness](#keyboard-awareness) |
75
+ | `footerAboveKeyboard` | `boolean` | `false` | With a fixed footer, float it above the keyboard instead of letting the keyboard cover it - see [Keyboard awareness](#keyboard-awareness) |
75
76
  | `headerFixed` | `boolean` | `false` | Pin header; overrides settings default |
76
77
  | `footerFixed` | `boolean` | `false` | Pin footer; overrides settings default |
77
78
  | `snapBack` | `boolean` | `false` | Snap header and footer back when scrolling up |
78
- | `snapBackHeader` | `boolean` | — | Override `snapBack` for header only |
79
- | `snapBackFooter` | `boolean` | — | Override `snapBack` for footer only |
80
- | `tabBarHeight` | `number` | `0` | Extra bottom inset reserved for a host app's own persistent tab bar (added on top of the safe-area inset, independent of any `ScrollViewFooter`) |
79
+ | `snapBackHeader` | `boolean` | - | Override `snapBack` for header only |
80
+ | `snapBackFooter` | `boolean` | - | Override `snapBack` for footer only |
81
+ | `tabBarHeight` | `number` | `0` | Extra bottom inset reserved for a host app's own persistent tab bar (added on top of the safe-area inset, independent of any `ScrollViewFooter`). Ignored inside a `ScrollViewChromeProvider` - the host's `footprint` replaces it (see [Persistent bottom chrome](#persistent-bottom-chrome-hiding-a-tab-bar-with-scroll)) |
82
+
83
+ ---
84
+
85
+ ### `ScrollViewChromeProvider`
86
+
87
+ Hosts a persistent bottom bar (a tab bar) so it hides on scroll together with each screen's `ScrollViewFooter`. Mount it once, above the navigator. Every focused `ScrollViewProvider` below it joins automatically. See [Persistent bottom chrome](#persistent-bottom-chrome-hiding-a-tab-bar-with-scroll) for the full picture.
88
+
89
+ | Prop | Type | Default | Description |
90
+ |------|------|---------|-------------|
91
+ | `children` | `ReactNode` | - | Usually the navigator (`<Tabs>`) |
92
+ | `footprint` | `number` | required | Height of the bar's resting box, including its bottom safe-area inset - how far above the screen bottom a hosted `ScrollViewFooter` has to sit to clear it. A plain number rather than something the bar reports back, so every screen has the right geometry on its very first render |
93
+ | `overhang` | `number` | `0` | How far the bar pokes *above* its resting box (a raised centre button, say). Hosted footers pad their bottom by this instead of the safe-area inset, and the hide distance covers it |
94
+ | `snapBack` | `boolean` | `true` | Hosted footers reveal on a short scroll-up instead of only near the top. A bar that hides on scroll has to be reachable from anywhere in a list, so leave this on unless you have a reason not to |
95
+ | `useIsActive` | `() => boolean` | `() => true` | A hook returning whether the calling screen is the focused one - pass react-navigation's `useIsFocused` (re-exported by `expo-router`). Only the focused screen drives the bar. Injected so this package carries no navigation dependency. **Must be a stable hook reference** (a module-level hook such as `useIsFocused`, never an inline arrow created per render): it is called as a hook on every render of every hosted provider |
96
+
97
+ The default `() => true` makes every mounted hosted screen claim the host, and the last one to mount owns it - only right for a navigator that keeps a single screen mounted. Use `useIsFocused` with `Tabs` or anything else that keeps visited screens alive.
81
98
 
82
99
  ---
83
100
 
@@ -87,18 +104,18 @@ Floating header with blur backdrop, title, back action, trailing action, and bui
87
104
 
88
105
  | Prop | Type | Default | Description |
89
106
  |------|------|---------|-------------|
90
- | `title` | `string` | — | Center title text |
91
- | `caption` | `string` | — | Subtitle below title |
92
- | `centerContent` | `ReactNode` | — | Replaces title/caption with custom content |
93
- | `children` | `ReactNode` | — | Left-side content (icon buttons, etc.) |
94
- | `backAction` | `() => void` | — | Renders a back button and calls this on press |
107
+ | `title` | `string` | - | Center title text |
108
+ | `caption` | `string` | - | Subtitle below title |
109
+ | `centerContent` | `ReactNode` | - | Replaces title/caption with custom content |
110
+ | `children` | `ReactNode` | - | Left-side content (icon buttons, etc.) |
111
+ | `backAction` | `() => void` | - | Renders a back button and calls this on press |
95
112
  | `backActionFixed` | `boolean` | settings | Keep back button visible as header scrolls away |
96
- | `trailingAction` | `ReactNode` | — | Right-side floating action |
113
+ | `trailingAction` | `ReactNode` | - | Right-side floating action |
97
114
  | `trailingActionFixed` | `boolean` | `true` | Keep trailing action visible as header scrolls away |
98
115
  | `actionSize` | `number` | `48` | Size of floating action buttons |
99
116
  | `iconSize` | `number` | `actionSize/2` | Icon size inside action buttons |
100
- | `actionStyle` | `ViewStyle` | — | Style applied to action button backgrounds |
101
- | `style` | `ViewStyle` | — | Style applied to the header content area |
117
+ | `actionStyle` | `ViewStyle` | - | Style applied to action button backgrounds |
118
+ | `style` | `ViewStyle` | - | Style applied to the header content area |
102
119
  | `topInset` | `boolean` | `true` | Pad for safe area top inset |
103
120
 
104
121
  ---
@@ -112,6 +129,8 @@ Floating footer with blur backdrop. Place it as a direct child of `ScrollViewPro
112
129
  | `children` | `ReactNode` | Footer content |
113
130
  | `style` | `ViewStyle` | Style applied to the inner row |
114
131
 
132
+ Inside a [`ScrollViewChromeProvider`](#scrollviewchromeprovider) the footer sits above the host's persistent bar and hides and reveals together with it.
133
+
115
134
  ---
116
135
 
117
136
  ### `ScrollView`
@@ -170,7 +189,7 @@ When `horizontal` and `pagingEnabled` are both true and `data` has more than one
170
189
 
171
190
  ### `SectionList`
172
191
 
173
- Drop-in for React Native's `SectionList`. Accepts all `SectionListProps<ItemT, SectionT>` (except `horizontal`) plus the same set of library props as `FlatList`. Custom sticky section headers are supported — pass `renderSectionHeader` and `stickySectionHeadersEnabled` and the library renders them via an animated overlay so they properly account for the floating header offset.
192
+ Drop-in for React Native's `SectionList`. Accepts all `SectionListProps<ItemT, SectionT>` (except `horizontal`) plus the same set of library props as `FlatList`. Custom sticky section headers are supported - pass `renderSectionHeader` and `stickySectionHeadersEnabled` and the library renders them via an animated overlay so they properly account for the floating header offset.
174
193
 
175
194
  ---
176
195
 
@@ -199,7 +218,7 @@ import { CustomList } from '@rific/scroll-view'
199
218
  | `renderFilters` | `ReactNode` | Rendered below `ListHeaderComponent`, above list items |
200
219
  | `scrollRef` | `RefObject<{ scrollToOffset }>` | Forward ref to the underlying list |
201
220
 
202
- The underlying component receives `contentInset`, `contentOffset`, `onScroll`, `refreshControl`, and `scrollEventThrottle` — props it must support for the library to function.
221
+ The underlying component receives `contentInset`, `contentOffset`, `onScroll`, `refreshControl`, and `scrollEventThrottle` - props it must support for the library to function.
203
222
 
204
223
  ---
205
224
 
@@ -263,7 +282,7 @@ export default function SearchScreen() {
263
282
  | Prop | Type | Description |
264
283
  |------|------|-------------|
265
284
  | `onChangeText` | `(text: string) => void` | Called with trimmed search text |
266
- | `onHeightChange` | `(height: number) => void` | Required — pass the height back as `pullSearchHeight` |
285
+ | `onHeightChange` | `(height: number) => void` | Required - pass the height back as `pullSearchHeight` |
267
286
  | `value` | `string` | Controlled value |
268
287
  | `placeholder` | `string` | Placeholder text |
269
288
  | `debounce` | `boolean` | Debounce `onChangeText` by 500ms |
@@ -286,7 +305,7 @@ Pass `keyboardAware` to any scroll component (`ScrollView`, `FlatList`, `Section
286
305
  </ScrollViewProvider>
287
306
  ```
288
307
 
289
- By default, a fixed `ScrollViewFooter` doesn't follow the keyboard — it stays pinned to the screen bottom and the keyboard simply covers it. If the footer holds something the user needs while typing (a submit button, say), set `footerAboveKeyboard` on `ScrollViewProvider` so the footer floats above the keyboard instead, reachable without dismissing it first:
308
+ By default, a fixed `ScrollViewFooter` doesn't follow the keyboard - it stays pinned to the screen bottom and the keyboard simply covers it. If the footer holds something the user needs while typing (a submit button, say), set `footerAboveKeyboard` on `ScrollViewProvider` so the footer floats above the keyboard instead, reachable without dismissing it first:
290
309
 
291
310
  ```tsx
292
311
  <ScrollViewProvider footerFixed footerAboveKeyboard>
@@ -301,13 +320,133 @@ By default, a fixed `ScrollViewFooter` doesn't follow the keyboard — it stays
301
320
  </ScrollViewProvider>
302
321
  ```
303
322
 
304
- `footerAboveKeyboard` only has an effect when the footer is fixed (`footerFixed`) — a footer that scrolls away with content has nothing to reserve keyboard space for. Always pair it with `keyboardAware: true` on any scroll component that could have a focused input while it's visible. Content always reserves space for the current footer height regardless of `keyboardAware` (so a fixed footer never overlaps it) — and while floating, that height already includes the keyboard, so a list with `keyboardAware: false` ends up reserving keyboard space anyway, just indirectly through the footer rather than directly. There's no useful configuration where you'd want the footer reachable above the keyboard but *not* want that same list's own content clear of it too, so don't rely on `keyboardAware: false` to opt a list out of this while `footerAboveKeyboard` is on — it won't.
323
+ `footerAboveKeyboard` only has an effect when the footer is fixed (`footerFixed`) - a footer that scrolls away with content has nothing to reserve keyboard space for. Always pair it with `keyboardAware: true` on any scroll component that could have a focused input while it's visible. Content always reserves space for the current footer height regardless of `keyboardAware` (so a fixed footer never overlaps it) - and while floating, that height already includes the keyboard, so a list with `keyboardAware: false` ends up reserving keyboard space anyway, just indirectly through the footer rather than directly. There's no useful configuration where you'd want the footer reachable above the keyboard but *not* want that same list's own content clear of it too, so don't rely on `keyboardAware: false` to opt a list out of this while `footerAboveKeyboard` is on - it won't.
324
+
325
+ Floating is done by growing the footer's own container rather than translating it, so its `BlurView` backdrop always stays sealed against the true physical screen bottom - nothing shows through beneath it, including at the keyboard's rounded top corners.
326
+
327
+ While floating above an open keyboard, the footer also drops its safe-area bottom inset - it's no longer sitting at the physical screen edge, so that padding would just be dead space under its content. As the keyboard closes, the inset ramps back in continuously over the last bit of its travel rather than snapping back once it's fully closed, so the content settles at the safe-area line smoothly instead of dropping to the physical bottom edge and correcting back up.
328
+
329
+ Keyboard awareness is native-only - `useKeyboardInset` (and therefore `keyboardAware`/`footerAboveKeyboard`) always resolves to `0` height on web.
330
+
331
+ ---
332
+
333
+ ## Persistent bottom chrome (hiding a tab bar with scroll)
334
+
335
+ A `ScrollViewFooter` hides on scroll, but an app's tab bar lives outside this package - it stays put, and the footer has to be lifted above it with `tabBarHeight`. `ScrollViewChromeProvider` turns the two into one rigid stack: the screen's footer and the tab bar share a single animated offset, so they hide and reveal together as one piece, frame for frame.
336
+
337
+ Three pieces make it work:
338
+
339
+ 1. **Host** - mount `ScrollViewChromeProvider` above the navigator, giving it the bar's `footprint`.
340
+ 2. **Screens** - nothing to change. Each `ScrollViewProvider` (and its `ScrollViewHeader`, `ScrollViewFooter`, scroll component) below the host joins it while its screen is focused.
341
+ 3. **Bar** - your own tab bar reads the shared offset with `useScrollViewChromeStyle()` and applies it as an animated style. It has to overlay the screen (absolutely positioned at the bottom) with a resting box exactly `footprint` tall, since hosted footers are lifted by that number.
342
+
343
+ ```tsx
344
+ // app/(tabs)/_layout.tsx
345
+ import type { BottomTabBarProps } from '@react-navigation/bottom-tabs'
346
+ import { ScrollViewChromeProvider, useScrollViewChromeReveal, useScrollViewChromeStyle } from '@rific/scroll-view'
347
+ import { Tabs, useIsFocused } from 'expo-router'
348
+ import { Pressable, StyleSheet, Text } from 'react-native'
349
+ import Animated from 'react-native-reanimated'
350
+ import { useSafeAreaInsets } from 'react-native-safe-area-context'
351
+
352
+ const TAB_ROW = 48
353
+
354
+ const TabBar = ({ navigation, state }: BottomTabBarProps) => {
355
+ const insets = useSafeAreaInsets()
356
+ const chromeStyle = useScrollViewChromeStyle()
357
+ const reveal = useScrollViewChromeReveal()
358
+ return (
359
+ <Animated.View style={[styles.bar, { height: TAB_ROW + insets.bottom, paddingBottom: insets.bottom }, chromeStyle]}>
360
+ {state.routes.map((route, index) => (
361
+ <Pressable
362
+ key={route.key}
363
+ style={styles.tab}
364
+ onPress={() => {
365
+ navigation.navigate(route.name)
366
+ // Tapping the tab you're already on brings the bar back
367
+ if (state.index === index) reveal()
368
+ }}
369
+ >
370
+ <Text>{route.name}</Text>
371
+ </Pressable>
372
+ ))}
373
+ </Animated.View>
374
+ )
375
+ }
376
+
377
+ export default function TabsLayout() {
378
+ const insets = useSafeAreaInsets()
379
+ return (
380
+ <ScrollViewChromeProvider footprint={TAB_ROW + insets.bottom} useIsActive={useIsFocused}>
381
+ <Tabs screenOptions={{ headerShown: false }} tabBar={(props) => <TabBar {...props} />} />
382
+ </ScrollViewChromeProvider>
383
+ )
384
+ }
385
+
386
+ const styles = StyleSheet.create({
387
+ bar: { bottom: 0, flexDirection: 'row', left: 0, position: 'absolute', right: 0 },
388
+ tab: { alignItems: 'center', flex: 1, height: TAB_ROW, justifyContent: 'center' }
389
+ })
390
+ ```
391
+
392
+ Screens are written exactly as they were - there is no chrome-specific prop on the happy path:
393
+
394
+ ```tsx
395
+ // app/(tabs)/library.tsx
396
+ export default function Library() {
397
+ return (
398
+ <ScrollViewProvider>
399
+ <ScrollViewHeader title="Library" />
400
+ <FlatList data={items} renderItem={renderItem} keyExtractor={(item) => item.id} />
401
+ <ScrollViewFooter>
402
+ <Button onPress={addItem}>Add</Button>
403
+ </ScrollViewFooter>
404
+ </ScrollViewProvider>
405
+ )
406
+ }
407
+ ```
408
+
409
+ ### Pinning the bar
410
+
411
+ `useScrollViewChromePin(active)` holds the bar fully revealed - and stops scroll from hiding it - for as long as `active` is `true`. Use it whenever something on screen needs the bar to stay put, such as a focused search field or a multi-select in progress. Pins are counted, so two independent pinners can't release each other, and it works from any component under the host (below a `ScrollViewProvider` too).
412
+
413
+ ```tsx
414
+ function SearchField() {
415
+ const [focused, setFocused] = useState(false)
416
+ useScrollViewChromePin(focused)
417
+ return <Searchbar onBlur={() => setFocused(false)} onFocus={() => setFocused(true)} />
418
+ }
419
+ ```
420
+
421
+ `useScrollViewChromeReveal()` returns a function that animates the bar back to fully revealed (200ms) - handy for a tab press or a "scroll to top" action, as in the example above.
422
+
423
+ ### Nested providers that aren't screens
424
+
425
+ A `ScrollViewProvider` nested inside another one never sees the host: every hosted provider clears it for its own children, so a panel inside a screen can't compete with its parent. A picker or form modal is the exception - a portal can re-parent it out from under its screen's provider, and it would then claim the host and drive the bar from its own scrolling. Pass `chrome={false}` to opt those out:
426
+
427
+ ```tsx
428
+ <ScrollViewProvider chrome={false} footerFixed>
429
+ <ScrollViewHeader title="Choose a category" />
430
+ <FlatList data={categories} renderItem={renderCategory} keyExtractor={(item) => item.id} />
431
+ </ScrollViewProvider>
432
+ ```
433
+
434
+ `chrome={false}` gives you exactly the unhosted behavior described in the rest of this README.
305
435
 
306
- Floating is done by growing the footer's own container rather than translating it, so its `BlurView` backdrop always stays sealed against the true physical screen bottom — nothing shows through beneath it, including at the keyboard's rounded top corners.
436
+ ### What changes for a hosted screen
307
437
 
308
- While floating above an open keyboard, the footer also drops its safe-area bottom inset — it's no longer sitting at the physical screen edge, so that padding would just be dead space under its content. As the keyboard closes, the inset ramps back in continuously over the last bit of its travel rather than snapping back once it's fully closed, so the content settles at the safe-area line smoothly instead of dropping to the physical bottom edge and correcting back up.
438
+ - **The footer joins the stack.** It sits `footprint` above the screen bottom, translates by the shared offset (clamped to the full hide distance), and pads its bottom by `overhang` instead of the safe-area inset - the bar's `footprint` already includes that inset.
439
+ - **Hide distance.** The stack slides `footprint + max(footerHeight, overhang) + 2` points: the bar, plus the screen's footer (whose measured height already includes the `overhang` padding; with no footer, just the `overhang`), plus 2 points of slack so a soft shadow bleeding past the bar's box doesn't leave a sliver at the screen edge.
440
+ - **Footer snap-back is forced.** A hosted footer follows the host's `snapBack` (default `true`) whatever the provider, `snapBackFooter` or settings values say, so the stack reveals on a short scroll-up from anywhere in a list. The header's own snap-back is untouched.
441
+ - **Scrolling stops at an end.** Hiding tracks the scroll 1:1, so a drag can end with the stack half hidden. When a drag ends without momentum, or momentum ends, the stack animates (200ms) to whichever end is nearer - fully shown or fully hidden, never cut off.
442
+ - **Short lists never hide.** A list that can't scroll at least the hide distance would strand the stack half hidden, so it never starts hiding and stays revealed.
443
+ - **Footer Lock keeps it revealed.** A fixed footer (`footerFixed`, `fixed`, or the settings default) stops the scroll handlers from writing the offset, and turning it on while the stack is hidden animates the stack back. The hosted footer still reads the shared offset rather than pinning itself to zero, so the footer and the bar stay equal on every frame, including that animation.
444
+ - **The focused screen owns the bar.** Screens that are mounted but unfocused never write the offset. Whenever ownership changes - switching tabs, or landing on a screen with no `ScrollViewProvider` at all - the stack resets to fully revealed instead of staying wherever the last screen parked it. A pin engaging does the same.
445
+ - **The reserved bottom inset stays constant while hidden.** Scroll content reserves `footprint` plus the overhang (or a fixed footer's height) at the bottom. That space can't animate, so it stays reserved as the bar slides away rather than the list resizing mid-scroll - the last row can always scroll clear of a revealed bar, and nothing shifts when the bar hides.
446
+ - **`tabBarHeight` is ignored.** The host's `footprint` is the same "reserve this much at the bottom" number, just known exactly, and takes its place.
447
+ - **With a fixed footer floating above the keyboard** (`footerFixed` + `footerAboveKeyboard`), the footer sits above whichever is taller, the bar or the keyboard, instead of floating a whole bar-height above the keyboard.
309
448
 
310
- Keyboard awareness is native-only — `useKeyboardInset` (and therefore `keyboardAware`/`footerAboveKeyboard`) always resolves to `0` height on web.
449
+ Outside a host - or in a provider with `chrome={false}` - none of this applies and every behavior in the rest of this README is unchanged. `useScrollViewChromeStyle`, `useScrollViewChromePin` and `useScrollViewChromeReveal` are also inert there (a permanent zero translate, a no-op pin and a no-op reveal), so a shared tab bar component can call them unconditionally.
311
450
 
312
451
  ---
313
452
 
@@ -326,7 +465,7 @@ const { scrollPosition, scrollHeight, progressing, progress, setProgress, setPro
326
465
  | `scrollPosition` | `SharedValue<number>` | Live scroll offset (Reanimated worklet-safe) |
327
466
  | `scrollHeight` | `number` | Visible scroll area height (window minus header/footer) |
328
467
  | `progressing` | `boolean` | Whether the header progress bar is visible |
329
- | `progress` | `number \| null` | Progress bar value (0–1), or `null` for indeterminate |
468
+ | `progress` | `number \| null` | Progress bar value (0-1), or `null` for indeterminate |
330
469
  | `setProgress` | `(p: number \| null) => void` | Set progress; pass `null` for indeterminate |
331
470
  | `setProgressing` | `(b: boolean) => void` | Show/hide the progress bar |
332
471
 
@@ -363,6 +502,39 @@ const keyboardHeight = useKeyboardInset()
363
502
 
364
503
  ---
365
504
 
505
+ ### `useScrollViewChromeStyle`
506
+
507
+ For the persistent bar hosted by [`ScrollViewChromeProvider`](#scrollviewchromeprovider). Returns an animated style (`translateY` from the shared offset) to apply to an `Animated.View`, so the bar moves in lockstep with the focused screen's footer. Outside a host it is a permanent zero translate.
508
+
509
+ ```tsx
510
+ const chromeStyle = useScrollViewChromeStyle()
511
+ return <Animated.View style={[styles.bar, chromeStyle]}>{/* tabs */}</Animated.View>
512
+ ```
513
+
514
+ ---
515
+
516
+ ### `useScrollViewChromePin`
517
+
518
+ Holds the hosted bar fully revealed, and stops scroll from hiding it, while `active` is `true`. Pins are ref-counted, so independent pinners can't release each other. Inert outside a host.
519
+
520
+ ```tsx
521
+ useScrollViewChromePin(searchFocused)
522
+ ```
523
+
524
+ ---
525
+
526
+ ### `useScrollViewChromeReveal`
527
+
528
+ Returns a `() => void` that animates the hosted bar back to fully revealed (200ms). Returns a no-op outside a host.
529
+
530
+ ```tsx
531
+ const reveal = useScrollViewChromeReveal()
532
+ ```
533
+
534
+ See [Persistent bottom chrome](#persistent-bottom-chrome-hiding-a-tab-bar-with-scroll) for all three in context.
535
+
536
+ ---
537
+
366
538
  ## Redux integration
367
539
 
368
540
  If your app uses Redux, you can drive scroll settings from the store instead of (or in addition to) `ScrollViewSettingsProvider`.
@@ -395,8 +567,10 @@ store.dispatch(scrollViewActions.initialize({ snapBack: true, headerFixed: false
395
567
  | `headerFixed` | `false` | Pin header globally |
396
568
  | `footerFixed` | `false` | Pin footer globally |
397
569
  | `snapBack` | `false` | Snap header and footer back when scrolling up |
398
- | `snapBackHeader` | — | Override `snapBack` for header only |
399
- | `snapBackFooter` | — | Override `snapBack` for footer only |
570
+ | `snapBackHeader` | - | Override `snapBack` for header only |
571
+ | `snapBackFooter` | - | Override `snapBack` for footer only |
400
572
  | `backActionFixed` | `true` | Keep back button visible as header scrolls away |
401
573
 
402
574
  Settings cascade: `ScrollViewSettingsProvider` → `ScrollViewProvider` → individual scroll component props. More specific values always win.
575
+
576
+ One exception: inside a `ScrollViewChromeProvider`, a footer's snap-back comes from the host's `snapBack` prop rather than this cascade (see [Persistent bottom chrome](#persistent-bottom-chrome-hiding-a-tab-bar-with-scroll)).
package/dist/index.d.mts CHANGED
@@ -6,7 +6,7 @@ import { ChipProps as ChipProps$1 } from '@rific/auto-paper';
6
6
  import * as _rific_core from '@rific/core';
7
7
  import { SettingsContextValue, SettingsProviderProps } from '@rific/core';
8
8
  import * as react_native_reanimated from 'react-native-reanimated';
9
- import { SharedValue } from 'react-native-reanimated';
9
+ import { SharedValue, AnimatedStyle } from 'react-native-reanimated';
10
10
 
11
11
  type ChipProps = Omit<ChipProps$1, 'compact' | 'icon' | 'onPress' | 'children'> & {
12
12
  label?: ReactNode;
@@ -157,8 +157,20 @@ type ScrollViewProps = ScrollViewProps$1 & {
157
157
  declare const ScrollViewInner: ({ chipProps, chipThreshold, children, contentContainerStyle: externalContentContainerStyle, footerFixed: footerFixedProp, gesture, headerFixed: headerFixedProp, horizontal, keyboardAware, onChipPress, onMomentumScrollEnd: externalMomentumScrollEnd, onRefresh, onScrollBeginDrag: externalScrollBeginDrag, onScrollEndDrag: externalScrollEndDrag, pullSearchHeight, refreshing, style, ...props }: ScrollViewProps) => react.JSX.Element;
158
158
  declare const ScrollView: typeof ScrollViewInner;
159
159
 
160
+ type ScrollViewChromeProviderProps = {
161
+ children: ReactNode;
162
+ footprint: number;
163
+ overhang?: number;
164
+ snapBack?: boolean;
165
+ useIsActive?: () => boolean;
166
+ };
167
+ declare const ScrollViewChromeProvider: ({ children, footprint, overhang, snapBack, useIsActive }: ScrollViewChromeProviderProps) => react.JSX.Element;
168
+
160
169
  type ScrollViewContextType = {
161
170
  blur: boolean;
171
+ chromeHosted: boolean;
172
+ chromeOverhang: number;
173
+ chromeWritable: SharedValue<boolean>;
162
174
  footerAboveKeyboard: boolean;
163
175
  footerHeight: number | null;
164
176
  footerHeightShared: SharedValue<number>;
@@ -183,6 +195,7 @@ type ScrollViewContextType = {
183
195
  setHeaderHeight: (h: number | null) => void;
184
196
  setProgress: (p: number | null) => void;
185
197
  setProgressing: (b: boolean) => void;
198
+ stackHeightShared: SharedValue<number>;
186
199
  tabBarHeight: number;
187
200
  };
188
201
  declare const ScrollViewContext: react.Context<ScrollViewContextType>;
@@ -213,6 +226,7 @@ declare const ScrollViewHeader: ({ actionSize, actionStyle, backAction, backActi
213
226
 
214
227
  type ScrollViewProviderProps = {
215
228
  blur?: boolean;
229
+ chrome?: boolean;
216
230
  children: ReactNode;
217
231
  fixed?: boolean;
218
232
  footerAboveKeyboard?: boolean;
@@ -223,7 +237,7 @@ type ScrollViewProviderProps = {
223
237
  snapBackHeader?: boolean;
224
238
  tabBarHeight?: number;
225
239
  };
226
- declare const ScrollViewProvider: ({ blur, children, fixed, footerAboveKeyboard, footerFixed, headerFixed, snapBack, snapBackFooter, snapBackHeader, tabBarHeight }: ScrollViewProviderProps) => react.JSX.Element;
240
+ declare const ScrollViewProvider: ({ blur, chrome, children, fixed, footerAboveKeyboard, footerFixed, headerFixed, snapBack, snapBackFooter, snapBackHeader, tabBarHeight }: ScrollViewProviderProps) => react.JSX.Element;
227
241
 
228
242
  type ScrollViewSettingsProviderProps = SettingsProviderProps<ScrollViewSettings>;
229
243
  declare const ScrollViewSettingsProvider: (props: SettingsProviderProps<ScrollViewSettings>) => React.JSX.Element;
@@ -265,6 +279,10 @@ declare const useScrollView: () => {
265
279
  setProgressing: (b: boolean) => void;
266
280
  };
267
281
 
282
+ declare const useScrollViewChromeStyle: () => StyleProp<AnimatedStyle<StyleProp<ViewStyle>>>;
283
+ declare const useScrollViewChromePin: (active: boolean) => void;
284
+ declare const useScrollViewChromeReveal: () => () => void;
285
+
268
286
  declare const useScrollViewSettings: () => _rific_core.SettingsContextValue<ScrollViewSettings>;
269
287
 
270
- export { type ChipProps, CustomList, type CustomListProps, FlatList, type FlatListProps, PullSearch, type PullSearchHandle, type PullSearchProps, ScrollView, ScrollViewContext, type ScrollViewContextType, ScrollViewFooter, type ScrollViewFooterProps, ScrollViewHeader, type ScrollViewHeaderProps, type ScrollViewProps, ScrollViewProvider, type ScrollViewProviderProps, type ScrollViewSettings, ScrollViewSettingsContext, type ScrollViewSettingsContextType, ScrollViewSettingsProvider, type ScrollViewSettingsProviderProps, SectionList, type SectionListProps, defaultScrollViewSettings, scrollViewActions, scrollViewReducer, useKeyboardInset, useScrollView, useScrollViewSettings };
288
+ export { type ChipProps, CustomList, type CustomListProps, FlatList, type FlatListProps, PullSearch, type PullSearchHandle, type PullSearchProps, ScrollView, ScrollViewChromeProvider, type ScrollViewChromeProviderProps, ScrollViewContext, type ScrollViewContextType, ScrollViewFooter, type ScrollViewFooterProps, ScrollViewHeader, type ScrollViewHeaderProps, type ScrollViewProps, ScrollViewProvider, type ScrollViewProviderProps, type ScrollViewSettings, ScrollViewSettingsContext, type ScrollViewSettingsContextType, ScrollViewSettingsProvider, type ScrollViewSettingsProviderProps, SectionList, type SectionListProps, defaultScrollViewSettings, scrollViewActions, scrollViewReducer, useKeyboardInset, useScrollView, useScrollViewChromePin, useScrollViewChromeReveal, useScrollViewChromeStyle, useScrollViewSettings };
package/dist/index.d.ts CHANGED
@@ -6,7 +6,7 @@ import { ChipProps as ChipProps$1 } from '@rific/auto-paper';
6
6
  import * as _rific_core from '@rific/core';
7
7
  import { SettingsContextValue, SettingsProviderProps } from '@rific/core';
8
8
  import * as react_native_reanimated from 'react-native-reanimated';
9
- import { SharedValue } from 'react-native-reanimated';
9
+ import { SharedValue, AnimatedStyle } from 'react-native-reanimated';
10
10
 
11
11
  type ChipProps = Omit<ChipProps$1, 'compact' | 'icon' | 'onPress' | 'children'> & {
12
12
  label?: ReactNode;
@@ -157,8 +157,20 @@ type ScrollViewProps = ScrollViewProps$1 & {
157
157
  declare const ScrollViewInner: ({ chipProps, chipThreshold, children, contentContainerStyle: externalContentContainerStyle, footerFixed: footerFixedProp, gesture, headerFixed: headerFixedProp, horizontal, keyboardAware, onChipPress, onMomentumScrollEnd: externalMomentumScrollEnd, onRefresh, onScrollBeginDrag: externalScrollBeginDrag, onScrollEndDrag: externalScrollEndDrag, pullSearchHeight, refreshing, style, ...props }: ScrollViewProps) => react.JSX.Element;
158
158
  declare const ScrollView: typeof ScrollViewInner;
159
159
 
160
+ type ScrollViewChromeProviderProps = {
161
+ children: ReactNode;
162
+ footprint: number;
163
+ overhang?: number;
164
+ snapBack?: boolean;
165
+ useIsActive?: () => boolean;
166
+ };
167
+ declare const ScrollViewChromeProvider: ({ children, footprint, overhang, snapBack, useIsActive }: ScrollViewChromeProviderProps) => react.JSX.Element;
168
+
160
169
  type ScrollViewContextType = {
161
170
  blur: boolean;
171
+ chromeHosted: boolean;
172
+ chromeOverhang: number;
173
+ chromeWritable: SharedValue<boolean>;
162
174
  footerAboveKeyboard: boolean;
163
175
  footerHeight: number | null;
164
176
  footerHeightShared: SharedValue<number>;
@@ -183,6 +195,7 @@ type ScrollViewContextType = {
183
195
  setHeaderHeight: (h: number | null) => void;
184
196
  setProgress: (p: number | null) => void;
185
197
  setProgressing: (b: boolean) => void;
198
+ stackHeightShared: SharedValue<number>;
186
199
  tabBarHeight: number;
187
200
  };
188
201
  declare const ScrollViewContext: react.Context<ScrollViewContextType>;
@@ -213,6 +226,7 @@ declare const ScrollViewHeader: ({ actionSize, actionStyle, backAction, backActi
213
226
 
214
227
  type ScrollViewProviderProps = {
215
228
  blur?: boolean;
229
+ chrome?: boolean;
216
230
  children: ReactNode;
217
231
  fixed?: boolean;
218
232
  footerAboveKeyboard?: boolean;
@@ -223,7 +237,7 @@ type ScrollViewProviderProps = {
223
237
  snapBackHeader?: boolean;
224
238
  tabBarHeight?: number;
225
239
  };
226
- declare const ScrollViewProvider: ({ blur, children, fixed, footerAboveKeyboard, footerFixed, headerFixed, snapBack, snapBackFooter, snapBackHeader, tabBarHeight }: ScrollViewProviderProps) => react.JSX.Element;
240
+ declare const ScrollViewProvider: ({ blur, chrome, children, fixed, footerAboveKeyboard, footerFixed, headerFixed, snapBack, snapBackFooter, snapBackHeader, tabBarHeight }: ScrollViewProviderProps) => react.JSX.Element;
227
241
 
228
242
  type ScrollViewSettingsProviderProps = SettingsProviderProps<ScrollViewSettings>;
229
243
  declare const ScrollViewSettingsProvider: (props: SettingsProviderProps<ScrollViewSettings>) => React.JSX.Element;
@@ -265,6 +279,10 @@ declare const useScrollView: () => {
265
279
  setProgressing: (b: boolean) => void;
266
280
  };
267
281
 
282
+ declare const useScrollViewChromeStyle: () => StyleProp<AnimatedStyle<StyleProp<ViewStyle>>>;
283
+ declare const useScrollViewChromePin: (active: boolean) => void;
284
+ declare const useScrollViewChromeReveal: () => () => void;
285
+
268
286
  declare const useScrollViewSettings: () => _rific_core.SettingsContextValue<ScrollViewSettings>;
269
287
 
270
- export { type ChipProps, CustomList, type CustomListProps, FlatList, type FlatListProps, PullSearch, type PullSearchHandle, type PullSearchProps, ScrollView, ScrollViewContext, type ScrollViewContextType, ScrollViewFooter, type ScrollViewFooterProps, ScrollViewHeader, type ScrollViewHeaderProps, type ScrollViewProps, ScrollViewProvider, type ScrollViewProviderProps, type ScrollViewSettings, ScrollViewSettingsContext, type ScrollViewSettingsContextType, ScrollViewSettingsProvider, type ScrollViewSettingsProviderProps, SectionList, type SectionListProps, defaultScrollViewSettings, scrollViewActions, scrollViewReducer, useKeyboardInset, useScrollView, useScrollViewSettings };
288
+ export { type ChipProps, CustomList, type CustomListProps, FlatList, type FlatListProps, PullSearch, type PullSearchHandle, type PullSearchProps, ScrollView, ScrollViewChromeProvider, type ScrollViewChromeProviderProps, ScrollViewContext, type ScrollViewContextType, ScrollViewFooter, type ScrollViewFooterProps, ScrollViewHeader, type ScrollViewHeaderProps, type ScrollViewProps, ScrollViewProvider, type ScrollViewProviderProps, type ScrollViewSettings, ScrollViewSettingsContext, type ScrollViewSettingsContextType, ScrollViewSettingsProvider, type ScrollViewSettingsProviderProps, SectionList, type SectionListProps, defaultScrollViewSettings, scrollViewActions, scrollViewReducer, useKeyboardInset, useScrollView, useScrollViewChromePin, useScrollViewChromeReveal, useScrollViewChromeStyle, useScrollViewSettings };