diff --git a/src/ReactFullpageSlideshow.tsx b/src/ReactFullpageSlideshow.tsx
index 585f7ec..357cb73 100644
--- a/src/ReactFullpageSlideshow.tsx
+++ b/src/ReactFullpageSlideshow.tsx
@@ -1,14 +1,35 @@
-import React, { useCallback, useRef, useState } from "react";
+import React, { useCallback, useEffect, useRef, useState } from "react";
import { GoToSlide, ReactFullpageSlideshowItem, rfsApi } from "./types";
+import { isMouseEvent, isPointerEvent } from "./typeguards";
export default function ReactFullpageSlideshow({
items,
+ itemClassName = "",
+ slideAnimationMs = 1000,
+ swipeMinThresholdMs = 50,
+ swipeMaxThresholdMs = 300,
+ swipeMinDistance = 100,
}: {
items: ReactFullpageSlideshowItem[];
+ itemClassName?: string;
+ slideAnimationMs?: number;
+ swipeMinThresholdMs?: number;
+ swipeMaxThresholdMs?: number;
+ swipeMinDistance?: number;
}) {
+ // activeIndex ref and state should always be set together.
const activeIndexRef = useRef(0);
const [activeIndex, setActiveIndex] = useState(0);
+ // yOffset ref and state should always be set together.
+ const yOffsetRef = useRef(0);
+ const [yOffset, setYOffset] = useState(0);
+
+ // keep track of when/where a pointer or touch event started
+ const pointerStartData = useRef<
+ undefined | { timestamp: number; y: number }
+ >();
+
const goToSlide = useCallback(
(index: number) => {
if (index >= 0 && index < items.length) {
@@ -19,12 +40,105 @@ export default function ReactFullpageSlideshow({
[setActiveIndex],
);
+ const pointerDownCb = useCallback(
+ (event: PointerEvent | TouchEvent | MouseEvent) => {
+ let y = 0;
+
+ if (isPointerEvent(event) || isMouseEvent(event)) {
+ y = event.y;
+ } else {
+ y = event.changedTouches["0"].clientY;
+ }
+
+ pointerStartData.current = {
+ timestamp: Date.now(),
+ y,
+ };
+ },
+ [],
+ );
+
+ const pointerCancelCb = useCallback((e: unknown) => {
+ console.log(e);
+ console.log("cancelled?");
+ }, []);
+
+ const pointerUpCb = useCallback(
+ (event: PointerEvent | TouchEvent | MouseEvent) => {
+ setYOffset(0);
+
+ let y = 0;
+ if (isPointerEvent(event) || isMouseEvent(event)) {
+ y = event.y;
+ } else {
+ y = event.changedTouches["0"].clientY;
+ }
+
+ if (!pointerStartData.current) return;
+ const currentTs = Date.now();
+ const isSwipe =
+ currentTs - pointerStartData.current?.timestamp < swipeMaxThresholdMs &&
+ currentTs - pointerStartData.current?.timestamp > swipeMinThresholdMs &&
+ Math.abs(pointerStartData.current.y - y) > swipeMinDistance;
+ const isDragged =
+ Math.abs(yOffsetRef.current) >=
+ document.documentElement.clientHeight / 2;
+ if (isSwipe || isDragged) {
+ if (y < pointerStartData.current.y) {
+ goToSlide(activeIndexRef.current + 1);
+ } else {
+ goToSlide(activeIndexRef.current - 1);
+ }
+ }
+
+ yOffsetRef.current = 0;
+ pointerStartData.current = undefined;
+ },
+ [
+ goToSlide,
+ setYOffset,
+ swipeMaxThresholdMs,
+ swipeMinThresholdMs,
+ swipeMinDistance,
+ ],
+ );
+
+ useEffect(() => {
+ //addEventListener("wheel", wheelCb);
+
+ // TODO: feature detect
+ addEventListener("pointerdown", pointerDownCb);
+ addEventListener("pointerup", pointerUpCb);
+ //addEventListener("pointermove", pointerMoveCb);
+ addEventListener("pointercancel", pointerCancelCb);
+ addEventListener("touchstart", pointerDownCb);
+ addEventListener("touchcancel", pointerCancelCb);
+ //addEventListener("touchmove", pointerMoveCb);
+ addEventListener("touchend", pointerUpCb);
+
+ return () => {
+ //removeEventListener("wheel", wheelCb);
+
+ removeEventListener("pointerdown", pointerDownCb);
+ removeEventListener("pointerup", pointerUpCb);
+ //removeEventListener("pointermove", pointerMoveCb);
+ removeEventListener("pointercancel", pointerCancelCb);
+ removeEventListener("touchstart", pointerDownCb);
+ removeEventListener("touchcancel", pointerCancelCb);
+ //removeEventListener("touchmove", pointerMoveCb);
+ removeEventListener("touchend", pointerUpCb);
+ };
+ }, [pointerDownCb, pointerUpCb]);
+
const itemsWrapped = items.map((item, ind) => (
{item}
@@ -51,11 +165,17 @@ const SlideContainer = ({
index,
activeIndex,
goToSlide,
+ className,
+ slideAnimationMs,
+ yOffset,
}: {
children: ReactFullpageSlideshowItem;
index: number;
activeIndex: number;
goToSlide: GoToSlide;
+ className: string;
+ slideAnimationMs: number;
+ yOffset: number;
}) => {
const top = `${(index - activeIndex) * 100}vh`;
@@ -76,6 +196,7 @@ const SlideContainer = ({
return (
{children(api)}
diff --git a/src/Square.tsx b/src/Square.tsx
deleted file mode 100644
index d7493d7..0000000
--- a/src/Square.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import React from "react";
-
-export default function Square() {
- return ;
-}
diff --git a/src/index.ts b/src/index.ts
index eb4bfc0..511049b 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,4 +1,3 @@
import ReactFullpageSlideshow from "./ReactFullpageSlideshow";
-import Square from "./Square";
-export { Square, ReactFullpageSlideshow };
+export { ReactFullpageSlideshow };
diff --git a/src/typeguards.ts b/src/typeguards.ts
new file mode 100644
index 0000000..206fe0d
--- /dev/null
+++ b/src/typeguards.ts
@@ -0,0 +1,10 @@
+export function isTouchEvent(e: object): e is TouchEvent {
+ return "touches" in e;
+}
+
+export function isMouseEvent(e: object): e is MouseEvent {
+ return "clientY" in e && "ctrlKey" in e && e.ctrlKey !== undefined;
+}
+export function isPointerEvent(e: object): e is PointerEvent {
+ return "clientY" in e && "ctrlKey" in e && e.ctrlKey === undefined;
+}
diff --git a/test/BasicTest.test.tsx b/test/BasicTest.test.tsx
index e42e438..4d0ca1f 100644
--- a/test/BasicTest.test.tsx
+++ b/test/BasicTest.test.tsx
@@ -8,6 +8,10 @@ import { rfsApi } from "../src/types";
describe("BasicTest", () => {
it("renders", async () => {
const user = userEvent.setup();
+ jest
+ .spyOn(document.documentElement, "clientHeight", "get")
+ .mockImplementation(() => 500);
+
render();
expect(screen.getByText("slide 1").parentNode.parentNode).toHaveStyle(
@@ -98,6 +102,7 @@ describe("BasicTest", () => {
"top: 0vh",
);
+ // Clicking this should be a no-op because we are already on the last slide
await user.click(screen.getByText("Next-Slide-slide 5"));
expect(screen.getByText("slide 1").parentNode.parentNode).toHaveStyle(
@@ -208,7 +213,7 @@ describe("BasicTest", () => {
});
});
-const App = () => {
+export const App = () => {
return (
{
+ jest
+ .spyOn(document.documentElement, "clientHeight", "get")
+ .mockImplementation(() => 500);
+
+ render();
+ const main = screen.getByRole("main");
+
+ fireEvent(main, new PointerEvent("pointerup"));
+
+ expect(screen.getByText("slide 1").parentNode.parentNode).toHaveStyle(
+ "top: -100vh",
+ );
+ expect(screen.getByText("slide 2").parentNode.parentNode).toHaveStyle(
+ "top: 0vh",
+ );
+ expect(screen.getByText("slide 3").parentNode.parentNode).toHaveStyle(
+ "top: 100vh",
+ );
+ expect(screen.getByText("slide 4").parentNode.parentNode).toHaveStyle(
+ "top: 200vh",
+ );
+ expect(screen.getByText("slide 5").parentNode.parentNode).toHaveStyle(
+ "top: 300vh",
+ );
+});