@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 +21 -0
- package/README.md +199 -25
- package/dist/index.d.mts +21 -3
- package/dist/index.d.ts +21 -3
- package/dist/index.js +335 -156
- package/dist/index.mjs +311 -136
- package/package.json +4 -4
- package/src/CustomList.tsx +3 -3
- package/src/FlatList.tsx +4 -4
- package/src/PullSearch.tsx +1 -1
- package/src/ScrollView.tsx +4 -4
- package/src/ScrollViewChromeContext.ts +37 -0
- package/src/ScrollViewChromeProvider.tsx +65 -0
- package/src/ScrollViewContext.ts +17 -1
- package/src/ScrollViewFooter.tsx +56 -20
- package/src/ScrollViewHeader.tsx +31 -10
- package/src/ScrollViewProvider.tsx +100 -27
- package/src/SectionList.tsx +4 -4
- package/src/index.ts +3 -0
- package/src/internal/ChromeRegistrar.tsx +37 -0
- package/src/internal/HorizontalDots.tsx +1 -1
- package/src/internal/ScrollViewChip.tsx +3 -3
- package/src/internal/chrome.ts +19 -0
- package/src/internal/useScrollHandler.ts +26 -5
- package/src/internal/useScrollHandlerJS.ts +34 -10
- package/src/internal/useScrollList.ts +29 -12
- package/src/internal/useStickyHeaders.ts +3 -3
- package/src/redux/scrollViewSlice.ts +1 -1
- package/src/useKeyboardInset.ts +1 -1
- package/src/useScrollInit.ts +12 -12
- package/src/useScrollViewChrome.ts +41 -0
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`
|
|
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
|
|
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` |
|
|
79
|
-
| `snapBackFooter` | `boolean` |
|
|
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` |
|
|
91
|
-
| `caption` | `string` |
|
|
92
|
-
| `centerContent` | `ReactNode` |
|
|
93
|
-
| `children` | `ReactNode` |
|
|
94
|
-
| `backAction` | `() => void` |
|
|
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` |
|
|
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` |
|
|
101
|
-
| `style` | `ViewStyle` |
|
|
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
|
|
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`
|
|
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
|
|
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
|
|
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`)
|
|
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
|
-
|
|
436
|
+
### What changes for a hosted screen
|
|
307
437
|
|
|
308
|
-
|
|
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
|
-
|
|
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
|
|
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` |
|
|
399
|
-
| `snapBackFooter` |
|
|
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 };
|