From b95a7becad8f5ef3d975dab534fc26a29fb3d590 Mon Sep 17 00:00:00 2001 From: Petr Konecny Date: Wed, 26 Nov 2025 23:45:09 +0100 Subject: [PATCH 1/4] chore: introduce compound pattern --- example/examples/AnimatedExample.tsx | 5 ++--- example/examples/BasicExample.tsx | 6 +++--- example/examples/EnteringAnimationExample.tsx | 18 +++++++----------- example/examples/TimerPaginationExample.tsx | 10 +++------- example/examples/VideoCarouselExample.tsx | 10 +++------- example/examples/components/CarouselBase.tsx | 10 +++------- src/components/HeroCarousel/index.tsx | 11 +++++++++-- src/components/HeroCarouselSlide/index.tsx | 6 +++--- src/components/index.ts | 3 +-- src/context/SlideContext/index.tsx | 4 ++-- src/context/index.tsx | 2 +- src/utils/PausableTimeout.ts | 2 +- 12 files changed, 38 insertions(+), 49 deletions(-) diff --git a/example/examples/AnimatedExample.tsx b/example/examples/AnimatedExample.tsx index a2b0da9..e152c34 100644 --- a/example/examples/AnimatedExample.tsx +++ b/example/examples/AnimatedExample.tsx @@ -3,7 +3,6 @@ import { interpolateInsideCarousel, useCarouselContext, useAutoCarouselSlideIndex, - CarouselContextProvider, } from '@strv/react-native-hero-carousel' import { SafeAreaView, StyleSheet, View, Text, Dimensions } from 'react-native' import { Image } from 'expo-image' @@ -75,7 +74,7 @@ export default function AnimatedExample() { }, []) return ( - + @@ -88,7 +87,7 @@ export default function AnimatedExample() { - + ) } diff --git a/example/examples/BasicExample.tsx b/example/examples/BasicExample.tsx index 2f31d9f..f7e0450 100644 --- a/example/examples/BasicExample.tsx +++ b/example/examples/BasicExample.tsx @@ -1,4 +1,4 @@ -import { HeroCarousel, CarouselContextProvider } from '@strv/react-native-hero-carousel' +import { HeroCarousel } from '@strv/react-native-hero-carousel' import { SafeAreaView, StyleSheet, View, Text, Dimensions } from 'react-native' import { Image } from 'expo-image' import { LinearGradient } from 'expo-linear-gradient' @@ -38,7 +38,7 @@ export default function BasicExample() { }, []) return ( - + @@ -48,7 +48,7 @@ export default function BasicExample() { - + ) } diff --git a/example/examples/EnteringAnimationExample.tsx b/example/examples/EnteringAnimationExample.tsx index 0f79e7e..38aa38b 100644 --- a/example/examples/EnteringAnimationExample.tsx +++ b/example/examples/EnteringAnimationExample.tsx @@ -1,8 +1,4 @@ -import { - HeroCarousel, - CarouselContextProvider, - SlideAnimatedView, -} from '@strv/react-native-hero-carousel' +import { HeroCarousel } from '@strv/react-native-hero-carousel' import { SafeAreaView, StyleSheet, View, Text, Dimensions } from 'react-native' import { Image } from 'expo-image' import { LinearGradient } from 'expo-linear-gradient' @@ -36,17 +32,17 @@ const Slide = ({ image, title, index }: { image: string; title: string; index: n - + {title} - - + Animation: {animationNames[index % animationNames.length]} - + @@ -60,7 +56,7 @@ export default function EnteringAnimationExample() { }, []) return ( - + @@ -70,7 +66,7 @@ export default function EnteringAnimationExample() { - + ) } diff --git a/example/examples/TimerPaginationExample.tsx b/example/examples/TimerPaginationExample.tsx index db2b349..0c632e5 100644 --- a/example/examples/TimerPaginationExample.tsx +++ b/example/examples/TimerPaginationExample.tsx @@ -1,8 +1,4 @@ -import { - HeroCarousel, - CarouselContextProvider, - useAutoCarouselSlideIndex, -} from '@strv/react-native-hero-carousel' +import { HeroCarousel, useAutoCarouselSlideIndex } from '@strv/react-native-hero-carousel' import { SafeAreaView, StyleSheet, View, Text, Dimensions } from 'react-native' import { Image } from 'expo-image' import { LinearGradient } from 'expo-linear-gradient' @@ -63,7 +59,7 @@ export default function TimerPaginationExample() { } return ( - + @@ -80,7 +76,7 @@ export default function TimerPaginationExample() { - + ) } diff --git a/example/examples/VideoCarouselExample.tsx b/example/examples/VideoCarouselExample.tsx index 31d57a9..358355f 100644 --- a/example/examples/VideoCarouselExample.tsx +++ b/example/examples/VideoCarouselExample.tsx @@ -1,8 +1,4 @@ -import { - HeroCarousel, - CarouselContextProvider, - useAutoCarouselSlideIndex, -} from '@strv/react-native-hero-carousel' +import { HeroCarousel, useAutoCarouselSlideIndex } from '@strv/react-native-hero-carousel' import { SafeAreaView, StyleSheet, View, Text, Pressable, Dimensions, Platform } from 'react-native' import { useVideoPlayer, VideoView } from 'expo-video' import { LinearGradient } from 'expo-linear-gradient' @@ -89,7 +85,7 @@ const Slide = ({ videoUri, title, index }: { videoUri: string; title: string; in export default function VideoCarouselExample() { return ( - + @@ -100,7 +96,7 @@ export default function VideoCarouselExample() { - + ) } diff --git a/example/examples/components/CarouselBase.tsx b/example/examples/components/CarouselBase.tsx index dfbf1b3..59715b9 100644 --- a/example/examples/components/CarouselBase.tsx +++ b/example/examples/components/CarouselBase.tsx @@ -1,15 +1,11 @@ -import { - HeroCarousel, - HeroCarouselProps, - CarouselContextProvider, -} from '@strv/react-native-hero-carousel' +import { HeroCarousel, HeroCarouselProps } from '@strv/react-native-hero-carousel' import { SafeAreaView, StyleSheet, View } from 'react-native' import { Stack } from 'expo-router' import { Pagination } from '@/examples/components/Pagination' export function CarouselBase({ children }: { children: HeroCarouselProps['children'] }) { return ( - + @@ -17,7 +13,7 @@ export function CarouselBase({ children }: { children: HeroCarouselProps['childr - + ) } diff --git a/src/components/HeroCarousel/index.tsx b/src/components/HeroCarousel/index.tsx index 17d14a4..bd5b5e6 100644 --- a/src/components/HeroCarousel/index.tsx +++ b/src/components/HeroCarousel/index.tsx @@ -1,17 +1,18 @@ import React from 'react' -import { useCarouselContext } from '../../context/CarouselContext' +import { CarouselContextProvider, useCarouselContext } from '../../context/CarouselContext' import { HeroCarouselSlide } from '../HeroCarouselSlide' import { HeroCarouselAdapter } from '../AnimatedPagedView/Adapter' import { useAutoScroll } from '../../hooks/useAutoScroll' import { useInfiniteScroll } from '../../hooks/useInfiniteScroll' import { DEFAULT_INTERVAL } from './index.preset' +import { SlideAnimatedView } from 'components/SlideAnimatedView' export type HeroCarouselProps = { children: React.ReactNode[] } -export const HeroCarousel = ({ children }: HeroCarouselProps) => { +const HeroCarousel = ({ children }: HeroCarouselProps) => { const { scrollValue, userInteracted, @@ -72,3 +73,9 @@ export const HeroCarousel = ({ children }: HeroCarouselProps) => { ) } + +HeroCarousel.AnimatedView = SlideAnimatedView +HeroCarousel.Provider = CarouselContextProvider +HeroCarousel.Item = HeroCarouselSlide + +export { HeroCarousel } diff --git a/src/components/HeroCarouselSlide/index.tsx b/src/components/HeroCarouselSlide/index.tsx index 9277416..d756668 100644 --- a/src/components/HeroCarouselSlide/index.tsx +++ b/src/components/HeroCarouselSlide/index.tsx @@ -1,5 +1,5 @@ import { View } from 'react-native' -import { AutoCarouselSlideContext } from '../../context/SlideContext' +import { HeroCarouselSlideContext } from '../../context/SlideContext' import { useAutoScroll } from '../../hooks/useAutoScroll' import { useMemo } from 'react' import { useManualScroll } from '../../hooks/useManualScroll' @@ -21,14 +21,14 @@ export const HeroCarouselSlide = ({ }) => { return ( - ({ index, total, runAutoScroll, goToPage }), [index, total, runAutoScroll, goToPage], )} > {children} - + ) } diff --git a/src/components/index.ts b/src/components/index.ts index cd28e97..be36b76 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,2 +1 @@ -export * from './HeroCarousel' -export * from './SlideAnimatedView' +export { HeroCarousel, HeroCarouselProps } from './HeroCarousel' diff --git a/src/context/SlideContext/index.tsx b/src/context/SlideContext/index.tsx index c09bc39..d2f22f6 100644 --- a/src/context/SlideContext/index.tsx +++ b/src/context/SlideContext/index.tsx @@ -2,7 +2,7 @@ import { createContext, useContext } from 'react' import { useAutoScroll } from '../../hooks/useAutoScroll' import { useManualScroll } from '../../hooks/useManualScroll' -export const AutoCarouselSlideContext = createContext<{ +export const HeroCarouselSlideContext = createContext<{ index: number total: number runAutoScroll: ReturnType['runAutoScroll'] @@ -10,7 +10,7 @@ export const AutoCarouselSlideContext = createContext<{ } | null>(null) export const useAutoCarouselSlideIndex = () => { - const context = useContext(AutoCarouselSlideContext) + const context = useContext(HeroCarouselSlideContext) if (!context) { throw new Error('useAutoCarouselSlideIndex must be used within a AutoCarouselSlide') } diff --git a/src/context/index.tsx b/src/context/index.tsx index 93a6354..5b47641 100644 --- a/src/context/index.tsx +++ b/src/context/index.tsx @@ -1,2 +1,2 @@ -export * from './CarouselContext' +export { useCarouselContext } from './CarouselContext' export * from './SlideContext' diff --git a/src/utils/PausableTimeout.ts b/src/utils/PausableTimeout.ts index 9c9641e..939e681 100644 --- a/src/utils/PausableTimeout.ts +++ b/src/utils/PausableTimeout.ts @@ -20,7 +20,7 @@ export class PausableTimeout { callbackStartTime: number = 0 remaining: number = 0 paused: boolean = false - timerId: NodeJS.Timeout | null = null + timerId: ReturnType | null = null onPause?: (remaining: number) => void = () => {} onResume?: (remaining: number) => void = () => {} _callback: () => void From 35ebe5550d6df0c595d9ffcd0075d64f621cb04a Mon Sep 17 00:00:00 2001 From: Petr Konecny Date: Wed, 26 Nov 2025 23:50:03 +0100 Subject: [PATCH 2/4] chore: updated docs --- README.md | 191 +++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 154 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 410d105..871426f 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,8 @@ 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. -**✨ Context-Based Configuration** - All carousel settings are configured through the context provider for a clean, centralized API. +**✨ Compound Pattern** - Clean, intuitive API with `HeroCarousel.Provider`, `HeroCarousel.Item`, and `HeroCarousel.AnimatedView` +**✨ Context-Based Configuration** - All carousel settings are configured through the provider for a clean, centralized API. ## Features @@ -45,7 +46,7 @@ Make sure to follow the [React Native Reanimated installation guide](https://doc ```tsx import React from 'react' import { View, Text, StyleSheet } from 'react-native' -import { HeroCarousel, CarouselContextProvider } from '@strv/react-native-hero-carousel' +import { HeroCarousel } from '@strv/react-native-hero-carousel' const slides = [ { id: 1, title: 'Slide 1', color: '#FF6B6B' }, @@ -62,7 +63,7 @@ const Slide = ({ title, color }: { title: string; color: string }) => ( export default function BasicCarousel() { return ( - + {slides.map((slide) => ( @@ -70,7 +71,7 @@ export default function BasicCarousel() { ))} - + ) } @@ -95,12 +96,22 @@ const styles = StyleSheet.create({ ### Components -#### `CarouselContextProvider` +The `HeroCarousel` component uses a **compound pattern** that provides a clean, intuitive API: + +```tsx + + + {/* Your slide content */} + + +``` + +#### `HeroCarousel.Provider` The context provider that must wrap your carousel components. **All carousel configuration is passed here.** ```tsx - withTiming(to, { duration })} // Custom animation > {children} - + ``` **Props:** @@ -126,7 +137,7 @@ The context provider that must wrap your carousel components. **All carousel con #### `HeroCarousel` -The main carousel component that renders slides. **Takes no configuration props** - all configuration is handled by the context. +The main carousel component that renders slides. **Takes no configuration props** - all configuration is handled by the context provider. ```tsx @@ -142,6 +153,42 @@ The main carousel component that renders slides. **Takes no configuration props* | ---------- | ------------------- | ------------------------- | | `children` | `React.ReactNode[]` | Array of slide components | +#### `HeroCarousel.Item` + +A wrapper component for individual slides. Provides slide context to child components. **Note:** This is automatically used internally when you pass children to `HeroCarousel`, but you can use it directly for more control. + +```tsx +{/* Your slide content */} +``` + +#### `HeroCarousel.AnimatedView` + +A specialized animated view component that automatically handles entering/exiting animations based on carousel scroll position. Perfect for creating slide-specific animations. + +```tsx +import { FadeIn, FadeOut } from 'react-native-reanimated' +; + This animates when the slide becomes active + +``` + +**Props:** + +| Prop | Type | Default | Description | +| ------------------------- | -------------------------------------- | -------- | -------------------------------------------------------- | +| `children` | `React.ReactNode` | Required | Content to animate | +| `entering` | `AnimatedProps['entering']` | - | Entering animation (from react-native-reanimated) | +| `exiting` | `AnimatedProps['exiting']` | - | Exiting animation (from react-native-reanimated) | +| `layout` | `AnimatedProps['layout']` | - | Layout animation (from react-native-reanimated) | +| `enteringThreshold` | `number` | `0.99` | Threshold (0-1) when entering animation should trigger | +| `exitingThreshold` | `number` | `0.01` | Threshold (0-1) when exiting animation should trigger | +| `keepVisibleAfterExiting` | `boolean` | `false` | Keep component visible after exiting animation completes | +| `style` | `AnimatedProps['style']` | - | Additional styles | + ### Hooks #### `useCarouselContext()` @@ -161,12 +208,12 @@ const { scrollValue, timeoutValue, slideWidth, userInteracted, setUserInteracted - `userInteracted`: Boolean indicating if user has interacted with carousel - `setUserInteracted`: Function to update interaction state -#### `useHeroCarouselSlideIndex()` +#### `useAutoCarouselSlideIndex()` -Get the current slide information and auto-scroll controls. +Get the current slide information and auto-scroll controls. Must be used within a slide component (inside `HeroCarousel`). ```tsx -const { index, total, runAutoScroll, goToPage } = useHeroCarouselSlideIndex() +const { index, total, runAutoScroll, goToPage } = useAutoCarouselSlideIndex() ``` **Returns:** @@ -216,23 +263,25 @@ Then scan the QR code with Expo Go or run on simulator. See the [example app REA ### 📱 Available Examples -| Example | Description | Source Code | -| ---------------------- | --------------------------------------------------- | --------------------------------------------------------------------------------- | -| **Basic Carousel** | Simple auto-scrolling image carousel | [`BasicExample.tsx`](./example/examples/BasicExample.tsx) | -| **Animated Carousel** | Custom animations with scale, rotation, and opacity | [`AnimatedExample.tsx`](./example/examples/AnimatedExample.tsx) | -| **Video Carousel** | Video playback with play/pause controls | [`VideoCarouselExample.tsx`](./example/examples/VideoCarouselExample.tsx) | -| **Timer Pagination** | Visual progress indicators with custom intervals | [`TimerPaginationExample.tsx`](./example/examples/TimerPaginationExample.tsx) | -| **Entering Animation** | Advanced slide entrance animations | [`EnteringAnimationExample.tsx`](./example/examples/EnteringAnimationExample.tsx) | -| **Offset Example** | Custom slide positioning and spacing | [`OffsetExample.tsx`](./example/examples/OffsetExample.tsx) | +| Example | Description | Source Code | +| ---------------------- | -------------------------------------------------------------------- | --------------------------------------------------------------------------------- | +| **Basic Carousel** | Simple auto-scrolling image carousel | [`BasicExample.tsx`](./example/examples/BasicExample.tsx) | +| **Animated Carousel** | Custom animations with scale, rotation, and opacity | [`AnimatedExample.tsx`](./example/examples/AnimatedExample.tsx) | +| **Video Carousel** | Video playback with play/pause controls | [`VideoCarouselExample.tsx`](./example/examples/VideoCarouselExample.tsx) | +| **Timer Pagination** | Visual progress indicators with custom intervals | [`TimerPaginationExample.tsx`](./example/examples/TimerPaginationExample.tsx) | +| **Entering Animation** | Advanced slide entrance animations using `HeroCarousel.AnimatedView` | [`EnteringAnimationExample.tsx`](./example/examples/EnteringAnimationExample.tsx) | +| **Offset Example** | Custom slide positioning and spacing | [`OffsetExample.tsx`](./example/examples/OffsetExample.tsx) | ### 🎯 Key Example Features - **Image Carousels** with smooth transitions and auto-scrolling - **Video Integration** with `expo-video` and playback controls - **Custom Animations** using `interpolateInsideCarousel` utility +- **Entering/Exiting Animations** using `HeroCarousel.AnimatedView` component - **Timer-based Pagination** with visual progress bars - **Gesture Handling** with swipe navigation and user interaction detection - **Performance Optimization** with image preloading and memoization +- **Compound Pattern** - All examples use `HeroCarousel.Provider` for configuration ### 📍 Pagination Examples @@ -253,38 +302,87 @@ All pagination components automatically sync with the carousel state and support ### Configuration Examples -Different carousel configurations using the context provider: +Different carousel configurations using the compound pattern: ```tsx // Basic auto-scrolling carousel - + {slides} - + // Video carousel without auto-scroll - + {videoSlides} - + // Carousel with custom intervals per slide - (index + 1) * 2000}> + (index + 1) * 2000}> {slides} - + // Carousel starting from specific slide - + {slides} - + // Custom slide width and animation - withSpring(to, { damping: 15 })} > {slides} - + +``` + +### Using HeroCarousel.AnimatedView + +The `HeroCarousel.AnimatedView` component automatically handles entering/exiting animations based on carousel scroll position. Perfect for creating slide-specific animations: + +```tsx +import { HeroCarousel } from '@strv/react-native-hero-carousel' +import { FadeIn, FadeOut, SlideInDown } from 'react-native-reanimated' + +const Slide = ({ title, image }: { title: string; image: string }) => ( + + + + {/* Content that animates when slide becomes active */} + + {title} + + + {/* Multiple animated views with different timings */} + + Subtitle with delay + + +) + +// Usage + + + {slides.map((slide) => ( + + ))} + + ``` +**Key Features:** + +- Automatically triggers entering animation when slide becomes active +- Triggers exiting animation when slide leaves view +- Supports all Reanimated entering/exiting animations +- Configurable thresholds for animation timing +- Can keep content visible after exiting animation + ### Programmatic Navigation Control the carousel programmatically using the context: @@ -292,7 +390,7 @@ Control the carousel programmatically using the context: ```tsx const CarouselWithControls = () => { const { scrollValue, goToPage } = useCarouselContext() - const { runAutoScroll } = useHeroCarouselSlideIndex() + const { runAutoScroll } = useAutoCarouselSlideIndex() const goToNext = () => { runAutoScroll(0) // Immediate transition @@ -303,7 +401,7 @@ const CarouselWithControls = () => { } return ( - + {/* Your slides */} @@ -312,7 +410,7 @@ const CarouselWithControls = () => {