diff --git a/src/core/Slider.tsx b/src/core/Slider.tsx index 23da856dd..c6ad63a2d 100644 --- a/src/core/Slider.tsx +++ b/src/core/Slider.tsx @@ -1,49 +1,78 @@ import React, { useState, useEffect, useRef, ReactNode } from "react"; import Icon from "../core/Icon.tsx"; -import "./component.css"; +import "./Slider/component.css"; interface SliderProps { children: ReactNode[]; - interval?: number; + options?: { + interval?: number; + controlPosition?: "inline" | "floating"; + intervalIndicator?: boolean; + mqEnableThreshold?: () => boolean; + }; } interface SliderIndicatorProps { numSlides: number; activeIndex: number; interval: number; + intervalIndicator?: boolean; + isInline?: boolean; } const SlideIndicator = ({ numSlides, activeIndex, interval, + intervalIndicator, + isInline, }: SliderIndicatorProps) => { return ( -
- {Array.from({ length: numSlides }, (_, i) => ( -
- {i === activeIndex && ( -
- )} -
- ))} -
+ ); }; -const Slider = ({ children, interval = 15000 }: SliderProps) => { +const Slider = ({ children, options }: SliderProps) => { const [activeIndex, setActiveIndex] = useState(0); + const [touchStartX, setTouchStartX] = useState(0); + const [touchEndX, setTouchEndX] = useState(0); const timerRef = useRef(null); + const isInline = options?.controlPosition === "inline"; + const next = () => { setActiveIndex((prevIndex) => (prevIndex + 1) % children.length); resetInterval(); @@ -58,7 +87,24 @@ const Slider = ({ children, interval = 15000 }: SliderProps) => { const resetInterval = () => { if (timerRef.current) clearInterval(timerRef.current); - timerRef.current = setInterval(next, interval); + timerRef.current = setInterval(next, options?.interval ?? 10000); + }; + + const handleTouchStart = (e) => { + setTouchStartX(e.touches[0].clientX); + }; + + const handleTouchMove = (e) => { + setTouchEndX(e.touches[0].clientX); + }; + + const handleTouchEnd = () => { + if (touchStartX - touchEndX > 50) { + next(); + } + if (touchStartX - touchEndX < -50) { + prev(); + } }; useEffect(() => { @@ -66,10 +112,15 @@ const Slider = ({ children, interval = 15000 }: SliderProps) => { return () => { if (timerRef.current) clearInterval(timerRef.current); }; - }, [children.length, interval]); + }, [children.length, options?.interval]); return ( -
+
{ {children.map((child, index) => (
{child}
@@ -86,26 +140,53 @@ const Slider = ({ children, interval = 15000 }: SliderProps) => {
-
+
+ + +
- -
); }; diff --git a/src/core/Slider/Slider.stories.tsx b/src/core/Slider/Slider.stories.tsx index 53a410313..32397b750 100644 --- a/src/core/Slider/Slider.stories.tsx +++ b/src/core/Slider/Slider.stories.tsx @@ -14,7 +14,7 @@ const Slide = ({ name }: { name: string }) => (
- {/* */} + test-image

{name}

@@ -26,7 +26,7 @@ const Slide = ({ name }: { name: string }) => (
- {/* */} + test-image

Mentimeter

@@ -58,7 +58,31 @@ export default { component: Slider, args: { children: slides, - interval: 15000, + options: { + interval: 10000, + intervalIndicator: true, + controlPosition: "floating", + }, + }, +}; + +export const InlineControlPosition = { + args: { + options: { + interval: 10000, + intervalIndicator: true, + controlPosition: "inline", + }, + }, +}; + +export const WithoutIntervalIndicator = { + args: { + options: { + interval: 10000, + intervalIndicator: false, + controlPosition: "floating", + }, }, }; diff --git a/src/core/Slider/component.css b/src/core/Slider/component.css index 79bed4c69..a2201db36 100644 --- a/src/core/Slider/component.css +++ b/src/core/Slider/component.css @@ -1,3 +1,10 @@ +.ui-slider-marker { + font-size: 0.5rem; + top: -1px; + + @apply leading-none px-4 relative; +} + @keyframes fillAnimation { 0% { width: 0%; @@ -6,3 +13,26 @@ width: 100%; } } + +.ui-icon-cta { + @apply w-48 h-48 cursor-pointer overflow-hidden; + @apply rounded border-2 border-mid-grey hover:border-active-orange; + transition: all 0.4s; +} + +@screen md { + .ui-icon-cta-left:hover .ui-icon-cta-holder { + transform: translateX(-100%); + } + .ui-icon-cta-right .ui-icon-cta-holder { + transform: translateX(-100%); + } + .ui-icon-cta-right:hover .ui-icon-cta-holder { + transform: translateX(0%); + } +} + +.ui-icon-cta-holder { + @apply w-full h-full; + transition: all 0.4s; +} diff --git a/src/core/Slider/component.js b/src/core/Slider/component.js index e69de29bb..473eb5f28 100644 --- a/src/core/Slider/component.js +++ b/src/core/Slider/component.js @@ -0,0 +1,107 @@ +import "./component.css"; + +import throttle from "lodash.throttle"; + +import { queryId, queryIdAll } from "../dom-query"; + +const mdBreakpoint = () => window.matchMedia("(min-width: 48rem)").matches; +const DRAG_BUFFER = 20; + +const init = (slidesContainer) => { + const transformContainer = queryId("slider-strip", slidesContainer); + const slides = Array.from(queryIdAll("slider-slide", slidesContainer)); + const slideLeftChevron = queryId("slider-previous", slidesContainer); + const slideRightChevron = queryId("slider-next", slidesContainer); + const slideMarkers = Array.from(queryIdAll("slider-marker", slidesContainer)); + const sliderControls = queryId("slider-controls", slidesContainer); + + sliderControls.classList.replace("hidden", "flex"); + const slidesLength = slides.length; + + const slidesWidth = slidesContainer.getBoundingClientRect().width; + const { width: slideWidth, left: slideLeftDistance } = + slides[0].getBoundingClientRect(); + const { left: slideLeftDistanceSecond } = slides[1].getBoundingClientRect(); + const slideGap = slideLeftDistanceSecond - slideLeftDistance - slideWidth; + const adjustment = (slidesWidth - slideWidth) / 2; + + let currentIndex = 0; + let touchStartX = 0; + + const calculateTransform = (index) => + index * -slideWidth + adjustment + index * -slideGap; + + const updateSlide = (index) => + (transformContainer.style.transform = `translateX(${calculateTransform( + index + )}px)`); + + const updateMarkers = (index) => { + slideMarkers.forEach((marker) => + marker.classList.remove("text-active-orange") + ); + slideMarkers[index].classList.remove("text-cool-black"); + slideMarkers[index].classList.add("text-active-orange"); + }; + + const slideLeft = () => { + currentIndex = currentIndex - 1 <= 0 ? 0 : currentIndex - 1; + updateSlide(currentIndex); + updateMarkers(currentIndex); + }; + + const slideRight = () => { + currentIndex = + currentIndex + 1 >= slidesLength ? currentIndex : currentIndex + 1; + updateSlide(currentIndex); + updateMarkers(currentIndex); + }; + + updateSlide(0); + updateMarkers(0); + + slideLeftChevron.addEventListener("click", slideLeft); + + transformContainer.addEventListener("touchstart", (e) => { + touchStartX = e.touches[0]?.clientX; + }); + + transformContainer.addEventListener("touchend", (e) => { + const distance = e.changedTouches[0]?.clientX - touchStartX; + + // Prevent sliding on clicks + if (Math.abs(distance) < DRAG_BUFFER) return; + + const direction = distance > 0 ? slideLeft : slideRight; + direction(); + }); + + slideRightChevron.addEventListener("click", slideRight); + + return () => { + transformContainer.style.transform = null; + sliderControls.classList.replace("flex", "hidden"); + }; +}; + +const Slider = ({ container, mqEnableThreshold }) => { + if (!container) return; + + const breakpointCheck = mqEnableThreshold || (() => !mdBreakpoint()); + + let unmount = () => {}; + if (breakpointCheck()) unmount = init(container); + + window.addEventListener( + "resize", + throttle(() => { + if (breakpointCheck()) { + unmount = init(container); + } else { + unmount(); + } + }, 100) + ); +}; + +export default Slider;