diff --git a/README.md b/README.md index 04eebc2..1e1ef00 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,8 @@ - Full typescript support - Built in with Tailwind, but you can always customize the styles - Handling touch/mouse events -- Lazy image loading (WIP) -- Responsive support (WIP) +- Lazy image loading +- Responsive support ### How to use: @@ -41,3 +41,4 @@ - [ ] Add unit tests - [ ] Add examples - [x] Publish on NPM +- [ ] Add counter element diff --git a/package.json b/package.json index 89db5ec..91a5d27 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "author": "Paweł Stachula", "license": "MIT", "homepage": "https://simple-headless-carousel.onrender.com/", - "version": "0.0.9", + "version": "0.0.10", "sideEffects": false, "description": "React simple headless carousel", "type": "module", diff --git a/src/App.tsx b/src/App.tsx index 3235bc5..7f5bb37 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -21,16 +21,23 @@ function App() { autoPlayDelay={2000} autoPlay={false} slidesVisible={1} + lazy infinite step={1} total={4} > - + - + diff --git a/src/lib/simple-headless-carousel/components/Carousel.tsx b/src/lib/simple-headless-carousel/components/Carousel.tsx index bad5f94..c5b9ee9 100644 --- a/src/lib/simple-headless-carousel/components/Carousel.tsx +++ b/src/lib/simple-headless-carousel/components/Carousel.tsx @@ -16,6 +16,7 @@ import { clsx } from '../services/clsx'; import { useResizeObserver } from '../hooks/useResizeObserver'; const threashold = 0.25; +const fullSize = 100; type CarouselProps = { children: ReactNode; @@ -45,11 +46,12 @@ export const Carousel = memo( const { refWidth } = useResizeObserver(imgRef); const { total, slidesVisible, currentIndex, infinite } = state; - const totalWidth = (100 * total) / slidesVisible; + const totalWidth = (fullSize * total) / slidesVisible; const totalWidthPercent = `${totalWidth}%`; const width = (refWidth || 0) / total; - const cancelWrongTarget = (event: SlideEvent) => - event.target !== imgRef.current; + const cancelWrongTarget = ({ target }: SlideEvent) => { + return target !== imgRef.current; + }; const setCurrentIndex = useCallback( (value: number) => { @@ -61,11 +63,11 @@ export const Carousel = memo( const setTranslateX = useCallback( (x: number) => { animationRef.current = requestAnimationFrame(() => { - const percent = (x * 100) / width / total; + const percentX = (x * fullSize) / width / total; imgRef.current?.style.setProperty( 'transform', - `translateX(${percent}%)`, + `translateX(${percentX}%)`, ); }); }, diff --git a/src/lib/simple-headless-carousel/components/Counter.tsx b/src/lib/simple-headless-carousel/components/Counter.tsx new file mode 100644 index 0000000..0d89ed2 --- /dev/null +++ b/src/lib/simple-headless-carousel/components/Counter.tsx @@ -0,0 +1,3 @@ +export const Counter = () => { + return
Counter
; +}; diff --git a/src/lib/simple-headless-carousel/components/Slide.tsx b/src/lib/simple-headless-carousel/components/Slide.tsx index 58cab85..2f38790 100644 --- a/src/lib/simple-headless-carousel/components/Slide.tsx +++ b/src/lib/simple-headless-carousel/components/Slide.tsx @@ -1,5 +1,7 @@ -import { memo, type ReactNode } from 'react'; +import { memo, useContext, useRef, type ReactNode } from 'react'; import { clsx } from '../services/clsx'; +import { useIntersectionObserver } from '../hooks/useIntersectionObserver'; +import { CarouselContext } from '../context/CarouselContext'; type SlideProps = { children: ReactNode; @@ -13,8 +15,30 @@ type SlideProps = { * @param {ReactNode} children - The content of the slide. * @param {string} className - Additional CSS classes for the slide. */ -export const Slide = memo(({ children, className }: SlideProps) => ( -
- {children} -
-)); +export const Slide = memo(({ children, className }: SlideProps) => { + const { state } = useContext(CarouselContext); + const isVisible = useRef(false); + const intersectionRef = useRef(null); + const { entry } = useIntersectionObserver({ + ref: intersectionRef, + opts: { + root: null, + threshold: 0.5, + }, + }); + + if (!isVisible.current && entry?.isIntersecting) { + isVisible.current = entry.isIntersecting; + } + + const showLazy = state.lazy ? isVisible.current : true; + + return ( +
+ {showLazy && children} +
+ ); +}); diff --git a/src/lib/simple-headless-carousel/context/CarouselProvider.tsx b/src/lib/simple-headless-carousel/context/CarouselProvider.tsx index 9aa86ea..2c3c907 100644 --- a/src/lib/simple-headless-carousel/context/CarouselProvider.tsx +++ b/src/lib/simple-headless-carousel/context/CarouselProvider.tsx @@ -12,6 +12,7 @@ import { useMergeConfig } from '../hooks/useMergeConfig'; * * @param {ReactNode} children - The child components to be wrapped by the Carousel context. * @param {number} total - The total number of slides in the carousel. + * @param {boolean} lazy - Whether the carousel should lazy load images. * @param {boolean} autoPlay - Whether the carousel should automatically play. * @param {number} autoPlayDelay - The delay between each slide transition in auto play mode. * @param {number} slidesVisible - The number of slides visible at a time. diff --git a/src/lib/simple-headless-carousel/hooks/useCarouselReducer.ts b/src/lib/simple-headless-carousel/hooks/useCarouselReducer.ts index e622729..98c1563 100644 --- a/src/lib/simple-headless-carousel/hooks/useCarouselReducer.ts +++ b/src/lib/simple-headless-carousel/hooks/useCarouselReducer.ts @@ -18,6 +18,7 @@ export type CarouselState = { autoPlayDelay: number; autoPlay: boolean; infinite: boolean; + lazy: boolean; }; export type CarouselReduceDispatch = Dispatch; @@ -30,6 +31,7 @@ export const stateDefaults: CarouselState = { slidesVisible: 1, autoPlay: false, infinite: false, + lazy: true, }; export const carouselReducer = ( @@ -101,6 +103,7 @@ export const useCarouselReducer = (): { } => { const [state, dispatch] = useReducer(carouselReducer, { currentIndex: 0, + lazy: true, } as CarouselState); return { state, dispatch }; diff --git a/src/lib/simple-headless-carousel/hooks/useIntersectionObserver.ts b/src/lib/simple-headless-carousel/hooks/useIntersectionObserver.ts new file mode 100644 index 0000000..3a7f1a7 --- /dev/null +++ b/src/lib/simple-headless-carousel/hooks/useIntersectionObserver.ts @@ -0,0 +1,38 @@ +import { useEffect, useState, RefObject } from 'react'; + +type UseIntersectionObserverResult = { + entry?: IntersectionObserverEntry; +}; + +type UseIntersectionObserverProps = { + ref: RefObject; + opts: IntersectionObserverInit; +}; + +export const useIntersectionObserver = ({ + ref, + opts = {}, +}: UseIntersectionObserverProps): UseIntersectionObserverResult => { + const [entry, setEntry] = useState(); + + useEffect(() => { + if (!ref.current) return; + + const handleObserver = (entries: IntersectionObserverEntry[]) => { + for (const e of entries) { + if (e.target === ref.current) { + setEntry(e); + } + } + }; + + const observer = new IntersectionObserver(handleObserver, opts); + observer.observe(ref.current); + + return () => { + observer.disconnect(); + }; + }, [ref, opts.threshold, opts.root, opts.rootMargin]); + + return { entry }; +}; diff --git a/src/lib/simple-headless-carousel/hooks/useMergeConfig.ts b/src/lib/simple-headless-carousel/hooks/useMergeConfig.ts index 90edcec..4af9345 100644 --- a/src/lib/simple-headless-carousel/hooks/useMergeConfig.ts +++ b/src/lib/simple-headless-carousel/hooks/useMergeConfig.ts @@ -11,8 +11,8 @@ export const useMergeConfig = ({ autoPlayDelay, slidesVisible, step, + lazy, infinite, - width, total, }: Partial & { dispatch: CarouselReduceDispatch }) => { useEffect(() => { @@ -24,7 +24,7 @@ export const useMergeConfig = ({ autoPlay: autoPlay || stateDefaults.autoPlay, step: step || stateDefaults.step, infinite: infinite || stateDefaults.infinite, - width, + lazy: lazy || stateDefaults.lazy, total, }, }); @@ -35,7 +35,7 @@ export const useMergeConfig = ({ autoPlayDelay, slidesVisible, infinite, - width, + lazy, step, ]); }; diff --git a/src/lib/simple-headless-carousel/services/clsx.ts b/src/lib/simple-headless-carousel/services/clsx.ts index c9f3363..eaa4a32 100644 --- a/src/lib/simple-headless-carousel/services/clsx.ts +++ b/src/lib/simple-headless-carousel/services/clsx.ts @@ -1,2 +1,3 @@ -export const clsx = (...args: (string | boolean | undefined)[]) => - args.filter(Boolean).join(' '); +export const clsx = (...args: (string | boolean | undefined)[]) => { + return args.filter(Boolean).join(' '); +}; diff --git a/src/lib/simple-headless-carousel/services/getSliderClientX.ts b/src/lib/simple-headless-carousel/services/getSliderClientX.ts index 2e38154..656661c 100644 --- a/src/lib/simple-headless-carousel/services/getSliderClientX.ts +++ b/src/lib/simple-headless-carousel/services/getSliderClientX.ts @@ -1,4 +1,7 @@ import type { SlideEvent } from './types'; -export const getSlideClientX = (event: SlideEvent) => - event instanceof MouseEvent ? event.clientX : event.changedTouches[0].clientX; +export const getSlideClientX = (event: SlideEvent) => { + return event instanceof MouseEvent + ? event.clientX + : event.changedTouches[0].clientX; +};