Skip to content

Commit 70cc03a

Browse files
petrkonecny2Petr Konecny
andauthored
feat: add disable infinite scrolling (#14)
* feat: add option to disable infinite scrolling * fix: some additional fixes * chore: updated architecture * chore: updated architecture --------- Co-authored-by: Petr Konecny <[email protected]>
1 parent 62cf548 commit 70cc03a

File tree

15 files changed

+187
-68
lines changed

15 files changed

+187
-68
lines changed

README.md

Lines changed: 88 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
A highly customizable, performant carousel component for React Native with advanced animations, auto-scrolling capabilities, and infinite scrolling support. Built with React Native Reanimated for smooth, native-level performance.
44

5+
**✨ Context-Based Configuration** - All carousel settings are configured through the context provider for a clean, centralized API.
6+
57
## Features
68

79
**Auto-scrolling** with customizable intervals
@@ -55,7 +57,7 @@ const Slide = ({ title, color }: { title: string; color: string }) => (
5557

5658
export default function BasicCarousel() {
5759
return (
58-
<CarouselContextProvider>
60+
<CarouselContextProvider interval={4000} disableAutoScroll={false} initialIndex={0}>
5961
<View style={styles.container}>
6062
<HeroCarousel>
6163
{slides.map((slide) => (
@@ -90,38 +92,50 @@ const styles = StyleSheet.create({
9092

9193
#### `CarouselContextProvider`
9294

93-
The context provider that must wrap your carousel components.
95+
The context provider that must wrap your carousel components. **All carousel configuration is passed here.**
9496

9597
```tsx
9698
<CarouselContextProvider
97-
defaultScrollValue={1} // Initial scroll position (default: 1)
99+
initialIndex={0} // Initial slide index (default: 0)
98100
slideWidth={screenWidth} // Width of each slide (default: screen width)
101+
interval={3000} // Auto-scroll interval in ms
102+
disableAutoScroll={false} // Disable auto-scrolling
103+
disableInfiniteScroll={false} // Disable infinite scrolling
104+
autoScrollAnimation={(to, duration) => withTiming(to, { duration })} // Custom animation
99105
>
100106
{children}
101107
</CarouselContextProvider>
102108
```
103109

110+
**Props:**
111+
112+
| Prop | Type | Default | Description |
113+
| ----------------------- | ------------------------------------------ | ------------ | ----------------------------------------------------------------------------------- |
114+
| `initialIndex` | `number` | `0` | Initial slide index to start from |
115+
| `slideWidth` | `number` | screen width | Width of each slide in pixels |
116+
| `interval` | `number \| ((index: number) => number)` | `3000` | Auto-scroll interval in milliseconds, or function returning interval for each slide |
117+
| `disableAutoScroll` | `boolean` | `false` | Disable automatic scrolling |
118+
| `disableInfiniteScroll` | `boolean` | `false` | Disable infinite scrolling (shows first/last slide boundaries) |
119+
| `autoScrollAnimation` | `(to: number, duration: number) => number` | `withTiming` | Custom animation function for auto-scroll transitions |
120+
| `children` | `React.ReactNode` | Required | Carousel content (should contain HeroCarousel component) |
121+
104122
#### `HeroCarousel`
105123

106-
The main carousel component with auto-scrolling functionality.
124+
The main carousel component that renders slides. **Takes no configuration props** - all configuration is handled by the context.
107125

108126
```tsx
109-
<HeroCarousel
110-
interval={3000} // Auto-scroll interval in ms
111-
disableAutoScroll={false} // Disable auto-scrolling
112-
goToPageAnimation={(to, duration) => withTiming(to, { duration })} // Custom page transition animation
113-
>
114-
{children}
127+
<HeroCarousel>
128+
{slides.map((slide) => (
129+
<YourSlideComponent key={slide.id} {...slide} />
130+
))}
115131
</HeroCarousel>
116132
```
117133

118134
**Props:**
119135

120-
| Prop | Type | Default | Description |
121-
| ------------------- | --------------------------------------- | -------- | ----------------------------------------------------------------------------------- | --- |
122-
| `interval` | `number \| ((index: number) => number)` | `3000` | Auto-scroll interval in milliseconds, or function returning interval for each slide |
123-
| `disableAutoScroll` | `boolean` | `false` | Disable automatic scrolling | |
124-
| `children` | `React.ReactNode[]` | Required | Array of slide components |
136+
| Prop | Type | Description |
137+
| ---------- | ------------------- | ------------------------- |
138+
| `children` | `React.ReactNode[]` | Array of slide components |
125139

126140
### Hooks
127141

@@ -147,7 +161,7 @@ const { scrollValue, timeoutValue, slideWidth, userInteracted, setUserInteracted
147161
Get the current slide information and auto-scroll controls.
148162

149163
```tsx
150-
const { index, total, runAutoScroll, goToPage } = useAutoCarouselSlideIndex()
164+
const { index, total, runAutoScroll, goToPage } = useHeroCarouselSlideIndex()
151165
```
152166

153167
**Returns:**
@@ -232,32 +246,68 @@ All pagination components automatically sync with the carousel state and support
232246

233247
## Advanced Usage
234248

249+
### Configuration Examples
250+
251+
Different carousel configurations using the context provider:
252+
253+
```tsx
254+
// Basic auto-scrolling carousel
255+
<CarouselContextProvider interval={3000}>
256+
<HeroCarousel>{slides}</HeroCarousel>
257+
</CarouselContextProvider>
258+
259+
// Video carousel without auto-scroll
260+
<CarouselContextProvider disableAutoScroll>
261+
<HeroCarousel>{videoSlides}</HeroCarousel>
262+
</CarouselContextProvider>
263+
264+
// Carousel with custom intervals per slide
265+
<CarouselContextProvider interval={(index) => (index + 1) * 2000}>
266+
<HeroCarousel>{slides}</HeroCarousel>
267+
</CarouselContextProvider>
268+
269+
// Carousel starting from specific slide
270+
<CarouselContextProvider initialIndex={2} disableInfiniteScroll>
271+
<HeroCarousel>{slides}</HeroCarousel>
272+
</CarouselContextProvider>
273+
274+
// Custom slide width and animation
275+
<CarouselContextProvider
276+
slideWidth={300}
277+
autoScrollAnimation={(to, duration) => withSpring(to, { damping: 15 })}
278+
>
279+
<HeroCarousel>{slides}</HeroCarousel>
280+
</CarouselContextProvider>
281+
```
282+
235283
### Programmatic Navigation
236284

237285
Control the carousel programmatically using the context:
238286

239287
```tsx
240288
const CarouselWithControls = () => {
241-
const { scrollValue } = useCarouselContext()
242-
const { runAutoScroll } = useAutoCarouselSlideIndex()
289+
const { scrollValue, goToPage } = useCarouselContext()
290+
const { runAutoScroll } = useHeroCarouselSlideIndex()
243291

244292
const goToNext = () => {
245293
runAutoScroll(0) // Immediate transition
246294
}
247295

248296
const goToSlide = (slideIndex: number) => {
249-
scrollValue.value = withTiming(slideIndex, { duration: 500 })
297+
goToPage(slideIndex, 500) // Go to slide with 500ms animation
250298
}
251299

252300
return (
253-
<View>
254-
<HeroCarousel disableAutoScroll>{/* Your slides */}</HeroCarousel>
255-
256-
<View style={styles.controls}>
257-
<Button title="Previous" onPress={() => goToSlide(scrollValue.value - 1)} />
258-
<Button title="Next" onPress={goToNext} />
301+
<CarouselContextProvider disableAutoScroll>
302+
<View>
303+
<HeroCarousel>{/* Your slides */}</HeroCarousel>
304+
305+
<View style={styles.controls}>
306+
<Button title="Previous" onPress={() => goToSlide(scrollValue.value - 1)} />
307+
<Button title="Next" onPress={goToNext} />
308+
</View>
259309
</View>
260-
</View>
310+
</CarouselContextProvider>
261311
)
262312
}
263313
```
@@ -281,6 +331,18 @@ useEffect(() => {
281331
}, [])
282332
```
283333

334+
## Architecture
335+
336+
### Context-Based Configuration
337+
338+
This library uses a **context-based architecture** where all carousel configuration is passed to the `CarouselContextProvider` rather than individual components. This design provides several benefits:
339+
340+
**Centralized Configuration** - All settings in one place
341+
**Cleaner Component API** - Components focus on rendering, not configuration
342+
**Easier Testing** - Mock context for isolated component testing
343+
344+
That allows for components like pagination to not be attached to the carousel component.
345+
284346
## Troubleshooting
285347

286348
### Common Issues

example/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,12 +119,12 @@ Example template:
119119

120120
```tsx
121121
// examples/YourExample.tsx
122-
import { AutoCarousel, CarouselContextProvider } from '@strv/react-native-hero-carousel'
122+
import { HeroCarousel, CarouselContextProvider } from '@strv/react-native-hero-carousel'
123123

124124
export default function YourExample() {
125125
return (
126-
<CarouselContextProvider>
127-
<AutoCarousel>{/* Your slides */}</AutoCarousel>
126+
<CarouselContextProvider interval={3000} disableAutoScroll={false} initialIndex={0}>
127+
<HeroCarousel>{/* Your slides */}</HeroCarousel>
128128
</CarouselContextProvider>
129129
)
130130
}

example/examples/BasicExample.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@ const styles = StyleSheet.create({
5959
width: '100%',
6060
height: '100%',
6161
transformOrigin: 'center',
62-
transform: [{ scale: 1.6 }],
6362
},
6463
gradient: {
6564
position: 'absolute',

example/examples/EnteringAnimationExample.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,6 @@ const styles = StyleSheet.create({
8686
width: '100%',
8787
height: '100%',
8888
transformOrigin: 'center',
89-
transform: [{ scale: 1.6 }],
9089
},
9190
gradient: {
9291
position: 'absolute',

example/examples/OffsetExample.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,6 @@ const styles = StyleSheet.create({
148148
width: '100%',
149149
height: '100%',
150150
transformOrigin: 'center',
151-
transform: [{ scale: 1.6 }],
152151
},
153152
gradient: {
154153
position: 'absolute',

example/examples/TimerPaginationExample.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,10 @@ export default function TimerPaginationExample() {
5757
}
5858

5959
return (
60-
<CarouselContextProvider>
60+
<CarouselContextProvider interval={getInterval}>
6161
<SafeAreaView style={styles.container}>
6262
<View style={styles.container}>
63-
<HeroCarousel interval={getInterval}>
63+
<HeroCarousel>
6464
{images.map((image, index) => (
6565
<Slide
6666
key={index}
@@ -93,7 +93,6 @@ const styles = StyleSheet.create({
9393
width: '100%',
9494
height: '100%',
9595
transformOrigin: 'center',
96-
transform: [{ scale: 1.6 }],
9796
},
9897
gradient: {
9998
position: 'absolute',

example/examples/VideoCarouselExample.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,10 @@ const Slide = ({ videoUri, title, index }: { videoUri: string; title: string; in
9191

9292
export default function VideoCarouselExample() {
9393
return (
94-
<CarouselContextProvider>
94+
<CarouselContextProvider disableAutoScroll={true}>
9595
<SafeAreaView style={styles.container}>
9696
<View style={styles.container}>
97-
<HeroCarousel disableAutoScroll={true}>
97+
<HeroCarousel>
9898
{videos.map((video, index) => (
9999
<Slide key={index} videoUri={video} title={videoTitles[index]} index={index} />
100100
))}

src/components/AnimatedPagedView/index.tsx

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import React, { forwardRef, useImperativeHandle } from 'react'
1+
import React, { forwardRef, useCallback, useImperativeHandle } from 'react'
22
import { Dimensions } from 'react-native'
33
import Animated, {
44
useSharedValue,
55
useAnimatedStyle,
66
useAnimatedReaction,
77
runOnJS,
88
withTiming,
9+
clamp,
910
} from 'react-native-reanimated'
1011
import { Gesture, GestureDetector } from 'react-native-gesture-handler'
1112

@@ -27,14 +28,23 @@ export const AnimatedPagedView = forwardRef<AnimatedPagedScrollViewRef, Animated
2728
(props, ref) => {
2829
const translateX = useSharedValue(0)
2930
const context = useSharedValue({ x: 0 })
31+
const childrenArray = React.Children.toArray(props.children)
32+
33+
const clampValue = useCallback(
34+
(value: number) => {
35+
'worklet'
36+
return clamp(value, 0, (childrenArray.length - 1) * SCREEN_WIDTH)
37+
},
38+
[childrenArray.length],
39+
)
3040

3141
const gesture = Gesture.Pan()
3242
.onStart(() => {
3343
context.value = { x: translateX.value }
3444
runOnJS(props.onScrollBeginDrag)()
3545
})
3646
.onUpdate((event) => {
37-
translateX.value = context.value.x - event.translationX
47+
translateX.value = clampValue(context.value.x - event.translationX)
3848
})
3949
.onEnd((event) => {
4050
const velocity = event.velocityX
@@ -43,15 +53,16 @@ export const AnimatedPagedView = forwardRef<AnimatedPagedScrollViewRef, Animated
4353
velocity > 500 ? currentPage - 1 : velocity < -500 ? currentPage + 1 : currentPage
4454
// in case the gesture overshoots, snap to the nearest page
4555
if (Math.abs(context.value.x - translateX.value) > SCREEN_WIDTH / 2) {
46-
translateX.value = withTiming(currentPage * SCREEN_WIDTH)
56+
translateX.value = withTiming(clampValue(currentPage * SCREEN_WIDTH))
4757
} else {
48-
translateX.value = withTiming(targetPage * SCREEN_WIDTH)
58+
translateX.value = withTiming(clampValue(targetPage * SCREEN_WIDTH))
4959
}
5060
})
5161

5262
const animatedStyle = useAnimatedStyle(() => {
63+
const clampedTranslateX = clampValue(translateX.value)
5364
return {
54-
transform: [{ translateX: -translateX.value }],
65+
transform: [{ translateX: -clampedTranslateX }],
5566
}
5667
}, [])
5768

src/components/HeroCarousel/index.tsx

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,35 @@
11
import React from 'react'
22

3-
import { DEFAULT_INTERVAL } from './index.preset'
43
import { useCarouselContext } from '../../context/CarouselContext'
54
import { HeroCarouselSlide } from '../HeroCarouselSlide'
65
import { HeroCarouselAdapter } from '../AnimatedPagedView/Adapter'
76
import { useAutoScroll } from '../../hooks/useAutoScroll'
87
import { useInfiniteScroll } from '../../hooks/useInfiniteScroll'
9-
import { DEFAULT_ANIMATION } from '../../hooks/useManualScroll'
8+
import { DEFAULT_INTERVAL } from './index.preset'
109

1110
export type HeroCarouselProps = {
12-
interval?: number | ((index: number) => number)
1311
children: React.ReactNode[]
14-
autoScrollAnimation?: (to: number, duration: number) => number
15-
disableAutoScroll?: boolean
1612
}
1713

18-
export const HeroCarousel = ({
19-
interval = DEFAULT_INTERVAL,
20-
children,
21-
disableAutoScroll = false,
22-
autoScrollAnimation = DEFAULT_ANIMATION,
23-
}: HeroCarouselProps) => {
24-
const { scrollValue, userInteracted, slideWidth, timeoutValue, goToPage, manualScrollValue } =
25-
useCarouselContext()
14+
export const HeroCarousel = ({ children }: HeroCarouselProps) => {
15+
const {
16+
scrollValue,
17+
userInteracted,
18+
slideWidth,
19+
timeoutValue,
20+
goToPage,
21+
manualScrollValue,
22+
disableInfiniteScroll,
23+
interval,
24+
disableAutoScroll,
25+
autoScrollAnimation,
26+
} = useCarouselContext()
2627
const { paddedChildrenArray } = useInfiniteScroll({
2728
children,
2829
slideWidth,
2930
goToPage,
3031
scrollValue,
32+
disabled: disableInfiniteScroll,
3133
})
3234

3335
const autoScrollEnabled = !userInteracted
@@ -36,12 +38,13 @@ export const HeroCarousel = ({
3638
scrollValue,
3739
slideWidth,
3840
autoScrollEnabled,
39-
disableAutoScroll,
40-
interval,
41+
disableAutoScroll: disableAutoScroll ?? false,
42+
interval: interval ?? DEFAULT_INTERVAL,
4143
goToPage: (page: number, duration?: number) => {
4244
goToPage(page, duration, autoScrollAnimation)
4345
},
4446
timeoutValue,
47+
totalLength: paddedChildrenArray.length,
4548
})
4649

4750
return (

0 commit comments

Comments
 (0)