Skip to content

Commit

Permalink
feat: add responsive support and lazy img
Browse files Browse the repository at this point in the history
  • Loading branch information
pstachula-dev committed Jul 7, 2024
1 parent b1735e0 commit 2ca07c6
Show file tree
Hide file tree
Showing 12 changed files with 106 additions and 23 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -41,3 +41,4 @@
- [ ] Add unit tests
- [ ] Add examples
- [x] Publish on NPM
- [ ] Add counter element
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
11 changes: 9 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,23 @@ function App() {
autoPlayDelay={2000}
autoPlay={false}
slidesVisible={1}
lazy
infinite
step={1}
total={4}
>
<Carousel>
<Slide>
<Box index={0} />
<img
alt=""
src="https://cdn.brumly.pl/carsmarket/images/2024/5/clxarh1n9000hzfnpruix36ka/f030c788-4419-486e-b165-11d7f10766ee-1024x580.webp"
/>
</Slide>
<Slide>
<Box index={1} />
<img
alt=""
src="https://cdn.brumly.pl/carsmarket/images/2024/5/clxar7na90000zfnp982l5a0o/232c7e10-36e9-4c5c-8515-3dc0f038f949-1024x580.webp"
/>
</Slide>
<Slide>
<Box index={2} />
Expand Down
12 changes: 7 additions & 5 deletions src/lib/simple-headless-carousel/components/Carousel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) => {
Expand All @@ -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}%)`,
);
});
},
Expand Down
3 changes: 3 additions & 0 deletions src/lib/simple-headless-carousel/components/Counter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const Counter = () => {
return <div>Counter</div>;
};
36 changes: 30 additions & 6 deletions src/lib/simple-headless-carousel/components/Slide.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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) => (
<div className={clsx('pointer-events-none relative w-full', className)}>
{children}
</div>
));
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 (
<div
ref={intersectionRef}
className={clsx('pointer-events-none relative w-full', className)}
>
{showLazy && children}
</div>
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions src/lib/simple-headless-carousel/hooks/useCarouselReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export type CarouselState = {
autoPlayDelay: number;
autoPlay: boolean;
infinite: boolean;
lazy: boolean;
};

export type CarouselReduceDispatch = Dispatch<DispatchOpts>;
Expand All @@ -30,6 +31,7 @@ export const stateDefaults: CarouselState = {
slidesVisible: 1,
autoPlay: false,
infinite: false,
lazy: true,
};

export const carouselReducer = (
Expand Down Expand Up @@ -101,6 +103,7 @@ export const useCarouselReducer = (): {
} => {
const [state, dispatch] = useReducer(carouselReducer, {
currentIndex: 0,
lazy: true,
} as CarouselState);

return { state, dispatch };
Expand Down
38 changes: 38 additions & 0 deletions src/lib/simple-headless-carousel/hooks/useIntersectionObserver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { useEffect, useState, RefObject } from 'react';

type UseIntersectionObserverResult = {
entry?: IntersectionObserverEntry;
};

type UseIntersectionObserverProps<T> = {
ref: RefObject<T>;
opts: IntersectionObserverInit;
};

export const useIntersectionObserver = <T extends HTMLElement>({
ref,
opts = {},
}: UseIntersectionObserverProps<T>): UseIntersectionObserverResult => {
const [entry, setEntry] = useState<IntersectionObserverEntry>();

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]);

Check warning on line 35 in src/lib/simple-headless-carousel/hooks/useIntersectionObserver.ts

View workflow job for this annotation

GitHub Actions / Linters

React Hook useEffect has a missing dependency: 'opts'. Either include it or remove the dependency array

return { entry };
};
6 changes: 3 additions & 3 deletions src/lib/simple-headless-carousel/hooks/useMergeConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ export const useMergeConfig = ({
autoPlayDelay,
slidesVisible,
step,
lazy,
infinite,
width,
total,
}: Partial<CarouselState> & { dispatch: CarouselReduceDispatch }) => {
useEffect(() => {
Expand All @@ -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,
},
});
Expand All @@ -35,7 +35,7 @@ export const useMergeConfig = ({
autoPlayDelay,
slidesVisible,
infinite,
width,
lazy,
step,
]);
};
5 changes: 3 additions & 2 deletions src/lib/simple-headless-carousel/services/clsx.ts
Original file line number Diff line number Diff line change
@@ -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(' ');
};
7 changes: 5 additions & 2 deletions src/lib/simple-headless-carousel/services/getSliderClientX.ts
Original file line number Diff line number Diff line change
@@ -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;
};

0 comments on commit 2ca07c6

Please sign in to comment.