Skip to content

Commit

Permalink
feat: add docs and fix bug for cancelTarget
Browse files Browse the repository at this point in the history
  • Loading branch information
pstachula-dev committed Jul 11, 2024
1 parent 66f5096 commit 7bc5748
Show file tree
Hide file tree
Showing 7 changed files with 182 additions and 80 deletions.
84 changes: 74 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Simple Headless Carousel React


## Featues
- Extremely small package size: 2kB gzipped
- 0 external dependencies
- Full typescript support
Expand All @@ -8,19 +10,16 @@
- Lazy image loading
- Responsive support

### How to use:
## How to use:

```
<CarouselProvider total={3}>
<CarouselProvider total={2}>
<Carousel>
<Slide>
<Img />
</Slide>
<Slide>
<Img />
<img src="..." />
</Slide>
<Slide>
<Img />
<img src="..." />
</Slide>
</Carousel>
Expand All @@ -29,16 +28,81 @@
<NextButton />
</CarouselProvider>
```
## Props documentation:

### `<CarouselProvider />`
| Prop | Type | Default | Required |
| ---------|------|-----------|---------------|
| total | number | 0 | Yes |
| autoPlayDelay | number | false | No |
| slidesVisible | number | 1 | No |
| step | number | 1 | No |
| threshold | number | 0.25 | No |
| infinite | boolean | false | No |
| disableTouch | boolean | false | No |
| lazy | boolean | true | No |
| autoPlay | boolean | false | No |

### `<Carousel />`
| Prop | Type | Default | Required |
| ---------|------|-----------|---------------|
| children | ReactNode | | Yes |
| wrapperClassName | string | | No |
| carouselClassName | string | | No |

### `<Carousel />`
| Prop | Type | Default | Required |
| ---------|------|-----------|---------------|
| children | ReactNode | | Yes |
| className | string | | No |

### `<Dot />`
| Prop | Type | Default | Required |
| ---------|------|-----------|---------------|
| className | string | | No |
| colorActive | string | | No |
| colorInactive | string | | No |
| index | number | | Yes |
| disabled | boolean | | No |
| onClick | Function | | No |

### `<DotsGroup />`
| Prop | Type | Default | Required |
| ---------|------|-----------|---------------|
| className | string | | No |
| dotClassName | string | | No |
| colorActive | string | | No |
| colorInactive | string | | No |
| onClick | Function | | No |

### `<NextButton />`
| Prop | Type | Default | Required |
| ---------|------|-----------|---------------|
| className | string | | No |
| title | string | | No |
| onClick | Function | | No |

### `<PrevButton />`
| Prop | Type | Default | Required |
| ---------|------|-----------|---------------|
| className | string | | No |
| title | string | | No |
| onClick | Function | | No |

### `<Counter />`
| Prop | Type | Default | Required |
| ---------|------|-----------|---------------|
| className | string | | No |

### TODO:

- [x] Add beta implementation
- [ ] Add extra option props
- [ ] Add stable version
- [x] Add stable version
- [x] Setup CI
- [x] DX improvments prettier/eslint
- [ ] Add docs
- [x] Add docs
- [ ] Add unit tests
- [ ] Add examples
- [x] Publish on NPM
- [ ] Add counter element
- [x] Add counter element
11 changes: 11 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@
"version": "0.0.11",
"sideEffects": false,
"description": "React simple headless carousel",
"keywords": [
"react",
"reactjs",
"react-carousel",
"slider",
"tailwind",
"carousel",
"headless",
"simple",
"component"
],
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
11 changes: 4 additions & 7 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import { Counter } from './lib/simple-headless-carousel/components/Counter';

function App() {
return (
<div className="h-dvh w-full bg-gray-700">
<div className="mx-auto max-w-3xl pt-10">
<div className="h-dvh w-full bg-gray-800">
<div className="mx-auto flex max-w-3xl flex-col justify-center pt-10">
<CarouselProvider
autoPlayDelay={2000}
autoPlay={false}
Expand Down Expand Up @@ -37,15 +37,12 @@ function App() {
</Slide>
</Carousel>

<div className="my-4">
<DotsGroup />
</div>
<DotsGroup />

<div className="my-4">
<span>Couner:</span>
<span>Counter:</span>
<Counter />
</div>

<div className="mx-4 flex gap-4">
<PrevButton />
<NextButton />
Expand Down
55 changes: 24 additions & 31 deletions src/lib/simple-headless-carousel/components/Carousel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import {
useEffect,
useCallback,
useRef,
type ReactNode,
useMemo,
type ReactNode,
} from 'react';
import { CarouselContext } from '../context/CarouselContext';
import { getSlideClientX } from '../services/getSliderClientX';
Expand Down Expand Up @@ -63,20 +63,20 @@ export const Carousel = memo(
const setTranslateX = useCallback(
(x: number) => {
animationRef.current = requestAnimationFrame(() => {
const percentX = (x * fullSize) / width / total;
const percentX = (x * fullSize) / width / total / slidesVisible;

imgRef.current?.style.setProperty(
'transform',
`translateX(${percentX}%)`,
);
});
},
[width, total],
[width, total, slidesVisible],
);

const onMoveStart = useCallback((event: SlideEvent) => {
if (cancelWrongTarget(event)) return;
// Important due to blocking onMouseMove
// Important due to blocking onMouseMoev
event.preventDefault();

setIsMoving(false);
Expand All @@ -89,7 +89,7 @@ export const Carousel = memo(

const onMove = useCallback(
(event: SlideEvent) => {
if (!imgRef.current || isMoving || cancelWrongTarget(event)) return;
if (!imgRef.current || isMoving) return;

const startPosX = movePayload.current.startX;
const clientX = getSlideClientX(event);
Expand All @@ -109,37 +109,30 @@ export const Carousel = memo(
[currentIndex, width, isMoving, setTranslateX],
);

const onMoveEnd = useCallback(
(event: SlideEvent) => {
if (cancelWrongTarget(event)) return;
const onMoveEnd = useCallback(() => {
if (animationRef?.current) {
cancelAnimationFrame(animationRef.current);
}

if (animationRef?.current) {
cancelAnimationFrame(animationRef.current);
}
let finalIndex = currentIndex;

let finalIndex = currentIndex;
if (movePayload.current.clientX !== 0) {
const steps = Math.ceil(Math.abs(movePayload.current.clientX) / width);
const newIndex = movePayload.current.moveRight
? currentIndex - steps
: currentIndex + steps;

if (movePayload.current.clientX !== 0) {
const steps = Math.ceil(
Math.abs(movePayload.current.clientX) / width,
);
const newIndex = movePayload.current.moveRight
? currentIndex - steps
: currentIndex + steps;

if (infinite && (newIndex < 0 || newIndex >= total)) {
finalIndex = newIndex > 0 ? 0 : total - 1;
} else {
finalIndex = newIndex;
}
if (infinite && (newIndex < 0 || newIndex >= total)) {
finalIndex = newIndex > 0 ? 0 : total - 1;
} else {
finalIndex = newIndex;
}
}

setIsMoving(true);
setTranslateX(-width * finalIndex);
setCurrentIndex(finalIndex);
},
[setCurrentIndex, setTranslateX, currentIndex, infinite, total, width],
);
setIsMoving(true);
setTranslateX(-width * finalIndex);
setCurrentIndex(finalIndex);
}, [setCurrentIndex, setTranslateX, currentIndex, infinite, total, width]);

const eventsMap = useMemo(
(): EventMap => ({
Expand Down
50 changes: 33 additions & 17 deletions src/lib/simple-headless-carousel/components/Dot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { clsx } from '../services/clsx';
type DotProps = {
index: number;
className?: string;
colorActive?: string;
colorInactive?: string;
disabled?: boolean;
onClick?: (event: React.MouseEvent) => void;
};
Expand All @@ -14,25 +16,39 @@ type DotProps = {
*
* @param {number} index - The index of the dot.
* @param {string} className - The class name for the dot.
* @param {string} colorActive - The class name for the active dot.
* @param {string} colorInactive - The class name for the inactive dot.
* @param {boolean} disabled - Whether the dot is disabled.
* @param {Function} onClick - The callback function to be called when the button is clicked.
*/
export const Dot = memo(({ index, disabled, onClick, className }: DotProps) => {
const { dispatch, state } = useContext(CarouselContext);
const color = state.currentIndex === index ? 'bg-black' : 'bg-white';
export const Dot = memo(
({
index,
disabled,
onClick,
colorActive,
colorInactive,
className,
}: DotProps) => {
const { dispatch, state } = useContext(CarouselContext);
const color =
state.currentIndex === index
? colorActive || 'bg-black'
: colorInactive || 'bg-white';

const handleClick = (event: React.MouseEvent) => {
dispatch({ action: 'setCurrentIndex', value: index });
onClick?.(event);
};
const handleClick = (event: React.MouseEvent) => {
dispatch({ action: 'setCurrentIndex', value: index });
onClick?.(event);
};

return (
<button
type="button"
aria-label="Dot icon"
disabled={disabled}
onClick={handleClick}
className={clsx('size-3 rounded-full', color, className)}
/>
);
});
return (
<button
type="button"
aria-label="Dot icon"
disabled={disabled}
onClick={handleClick}
className={clsx('size-3 rounded-full', color, className)}
/>
);
},
);
49 changes: 35 additions & 14 deletions src/lib/simple-headless-carousel/components/DotsGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,47 @@ import { clsx } from '../services/clsx';

type DotsGroupProps = {
className?: string;
dotClassName?: string;
colorActive?: string;
colorInactive?: string;
onClick?: (event: React.MouseEvent) => void;
};

/**
* DotsGroup component for the carousel.
*
* @param {string} [className] - The class name for the dots group.
* @param {string} className - The class name for the dots group.
* @param {string} dotClassName - The class name for the single dot.
* @param {string} colorActive - The class name for the active dot.
* @param {string} colorInactive - The class name for the inactive dot.
* @param {Function} onClick - The callback function to be called when the button is clicked.
*/
export const DotsGroup = memo(({ onClick, className }: DotsGroupProps) => {
const id = useId();
const { state } = useContext(CarouselContext);
const { total, slidesVisible } = state;
const dotsLength = total / slidesVisible;
export const DotsGroup = memo(
({
onClick,
colorActive,
colorInactive,
dotClassName,
className,
}: DotsGroupProps) => {
const id = useId();
const { state } = useContext(CarouselContext);
const { total, slidesVisible } = state;
const dotsLength = total / slidesVisible;

return (
<div className={clsx('flex space-x-2', className)}>
{Array.from({ length: dotsLength }).map((_, idx) => (
<Dot onClick={onClick} key={id + idx} index={idx * slidesVisible} />
))}
</div>
);
});
return (
<div className={clsx('flex space-x-2', className)}>
{Array.from({ length: dotsLength }).map((_, idx) => (
<Dot
className={dotClassName}
colorActive={colorActive}
colorInactive={colorInactive}
onClick={onClick}
key={id + idx}
index={idx * slidesVisible}
/>
))}
</div>
);
},
);
Loading

0 comments on commit 7bc5748

Please sign in to comment.