From 76798736c92b0853b89c32d3d6ec7e62997c2487 Mon Sep 17 00:00:00 2001 From: rikhall1515 Date: Wed, 15 May 2024 10:10:43 +0200 Subject: [PATCH 1/2] feat(sidebar): add visible swipe to close functionality This change makes the swipe functionality visually make the sidebar move with exponential decay. --- .../header/sidebar/util/innerWrapper.tsx | 120 +++++++++++++--- doc/feat_sidebar.md | 136 ++++++++++++++++++ styles/base.css | 7 + styles/components.css | 4 + 4 files changed, 246 insertions(+), 21 deletions(-) create mode 100644 doc/feat_sidebar.md diff --git a/components/header/sidebar/util/innerWrapper.tsx b/components/header/sidebar/util/innerWrapper.tsx index d21af3b..5a3d43d 100644 --- a/components/header/sidebar/util/innerWrapper.tsx +++ b/components/header/sidebar/util/innerWrapper.tsx @@ -1,5 +1,5 @@ "use client"; -import { useState } from "react"; +import { useCallback, useRef, useEffect } from "react"; import { useHeaderContext } from "@/context/header"; import { useSidebarContext } from "@/context/sidebar"; @@ -10,17 +10,108 @@ export default function InnerWrapper({ children }: { children: React.ReactNode } const { isAtTop } = useHeaderContext(); const { isExpanded, toggle, sidebarRef } = useSidebarContext(); const { mainMenuBtnRef } = useMenuBtnRefContext(); - const [touchStart, setTouchStart] = useState(0); - const [touchEnd, setTouchEnd] = useState(0); - //127 - 72 = 55 / 2 = 22 - 23 + const touchStartXRef = useRef(0); + const animationFrameIdRef = useRef(0); // Ref to store the animation frame ID + const touchEndXRef = useRef(0); + + /** + * Exponentially decays the difference between end and start, + * inversed depending on which is biggerr, towards 100 or 40. + * + * This means that when dragging the sidebar menu to the left, it + * will only go 40px left before stopping after further dragging. + * + * If you drag it towards the right, it will go 100px in that direction + * before stopping. + * + * @argument {number} end + * @argument {number} start + * @returns {number} + */ + const directionAdjustedValue = useCallback((end: number, start: number) => { + if (end > start) { + return 100 - Math.exp(-0.04 * (end - start) + 4.6); //exponential decay towards 100px + } + return -40 + Math.exp(-0.04 * (start - end) + 3.8); //exponential decay towards 40px + }, []); + + const handleTouchStart = useCallback(function handleTouchStart(this: HTMLElement, e: TouchEvent) { + touchStartXRef.current = e.targetTouches[0].clientX; + }, []); + + const handleTouchMove = useCallback( + function handleTouchMove(this: HTMLElement, e: TouchEvent) { + touchEndXRef.current = e.targetTouches[0].clientX; + // Cancel any existing animation frame requests + if (animationFrameIdRef.current) { + cancelAnimationFrame(animationFrameIdRef.current); + } + + // Request a new animation frame + animationFrameIdRef.current = requestAnimationFrame(() => { + if (sidebarRef.current) { + sidebarRef.current.style.transform = `translateX(${directionAdjustedValue(touchEndXRef.current, touchStartXRef.current)}px)`; + } + }); + }, + [sidebarRef, directionAdjustedValue] + ); + + const handleTouchEnd = useCallback(() => { + // Cancel the animation frame request when touch ends + if (animationFrameIdRef.current) { + cancelAnimationFrame(animationFrameIdRef.current); + animationFrameIdRef.current = null; + } + // The difference between where the touch ends and begins needs to be greater than + // 100 in either direction. + if (Math.abs(touchEndXRef.current - touchStartXRef.current) > 100) { + toggle(); + } + // Reset transform so that the base classes kick into effect as normal + if (sidebarRef.current) { + sidebarRef.current.style.transform = ""; + } + }, [sidebarRef, toggle]); + + useEffect(() => { + const sidebar = sidebarRef.current; + if (!sidebar) return; + + // Adding event listeners. PreventDefault not needed so they are passive. + sidebar.addEventListener("touchstart", handleTouchStart, { passive: true }); + sidebar.addEventListener("touchmove", handleTouchMove, { passive: true }); + sidebar.addEventListener("touchend", handleTouchEnd, { passive: true }); + + // Cleanup function, makes sure that listeners are garbage collected + return () => { + sidebar.removeEventListener("touchstart", handleTouchStart); + sidebar.removeEventListener("touchmove", handleTouchMove); + sidebar.removeEventListener("touchend", handleTouchEnd); + + // Cancel any lingering animation frame requests + if (animationFrameIdRef.current) { + cancelAnimationFrame(animationFrameIdRef.current); + } + }; + }, [handleTouchStart, handleTouchMove, handleTouchEnd, sidebarRef]); return ( <> +