Skip to content

Commit

Permalink
feat/sidebar-swipe
Browse files Browse the repository at this point in the history
Implement visible swiping functionality to close sidebar.
  • Loading branch information
rikhall1515 authored May 15, 2024
2 parents e2c1ee2 + 09866ec commit f9d7b09
Show file tree
Hide file tree
Showing 6 changed files with 276 additions and 27 deletions.
32 changes: 29 additions & 3 deletions components/header/sidebar/nav/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { FaBoxArchive, FaFileLines, FaPassport } from "react-icons/fa6";

import { cn } from "@/lib/utils";

import Privacy from "./privacy";
import { SidebarItem } from "./sidebarItem";
export default function Nav() {
Expand All @@ -8,23 +10,47 @@ export default function Nav() {
<nav aria-label="In-page jump links" className="flex h-auto w-full flex-col font-bold">
{/* <LoginDashboard /> */}
<ul className="w-full">
<li className="w-full border-t-2 border-border">
<li
className={cn(
"relative w-full",
// "after:absolute after:bottom-[-0.125rem] after:h-[0.125rem] after:w-full",
// "after:bg-gradient-to-r after:from-border after:to-background after:to-50%",
"before:absolute before:top-[-0.125rem] before:h-[0.125rem] before:w-full",
"before:bg-gradient-to-r before:from-border before:to-background before:to-50%"
)}
>
<SidebarItem href="/archive">
<div className="ml-[2.125rem] w-6 fill-[inherit] stroke-[inherit]">
<FaBoxArchive className="h-6 w-6" />
</div>
<span>Archive</span>
</SidebarItem>
</li>
<li className="w-full border-t-2 border-border">
<li
className={cn(
"relative w-full",
"after:absolute after:bottom-[-0.125rem] after:h-[0.125rem] after:w-full",
"after:bg-gradient-to-r after:from-border after:to-background after:to-50%",
"before:absolute before:top-[-0.125rem] before:h-[0.125rem] before:w-full",
"before:bg-gradient-to-r before:from-border before:to-background before:to-50%"
)}
>
<SidebarItem href="/terms">
<div className="ml-[2.125rem] w-6 fill-[inherit] stroke-[inherit]">
<FaFileLines className="h-6 w-6" />
</div>
<span>Terms of Service</span>
</SidebarItem>
</li>
<li className="w-full border-y-2 border-border">
<li
className={cn(
"relative w-full",
"after:absolute after:bottom-[-0.125rem] after:h-[0.125rem] after:w-full",
"after:bg-gradient-to-r after:from-border after:to-background after:to-50%"
// "before:absolute before:top-[-0.125rem] before:h-[0.125rem] before:w-full",
// "before:bg-gradient-to-r before:from-border before:to-background before:to-50%"
)}
>
<Privacy>
<div className="ml-[2.125rem] w-6">
<FaPassport className="h-6 w-6" />
Expand Down
6 changes: 4 additions & 2 deletions components/header/sidebar/nav/sidebarItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ export const SidebarItem = memo(function SidebarItem({
className={cn(
"group/sidebarItem relative h-[3.75rem] w-full transition-all",
"flex items-center gap-3",
pathname === href ? "bg-primary text-primary-foreground" : "",
"hover:bg-primary"
pathname === href
? "bg-gradient-to-r from-primary to-background to-80% text-primary-foreground"
: "",
"hover:bg-gradient-to-r hover:from-primary hover:via-primary hover:via-40% hover:to-background hover:to-60% hover:text-primary-foreground"
)}
tabIndex={isExpanded ? 0 : -1}
onClick={toggle}
Expand Down
2 changes: 1 addition & 1 deletion components/header/sidebar/topbar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export default function TopBar() {
"relative box-border flex items-center justify-between",
"mb-12 h-[4.5rem] w-full",
"after:absolute after:bottom-[-0.125rem] after:h-[0.125rem] after:w-full",
"via-50% after:bg-gradient-to-r after:from-background after:via-primary after:to-background"
"after:bg-gradient-to-r after:from-primary after:to-background after:to-50%"
)}
>
<SearchButton />
Expand Down
120 changes: 99 additions & 21 deletions components/header/sidebar/util/innerWrapper.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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<number | null>(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 (
<>
<div
className={cn(
"fixed bottom-0 right-0 top-0 z-[29]",
"h-[100svh] w-[6.5rem] transition-all duration-500 ease-customEase",
"bg-background outline-none",
isExpanded
? "pointer-events-auto translate-x-0 opacity-100"
: "pointer-events-none translate-x-full opacity-0"
)}
/>
<aside
className={cn(
"fixed bottom-0 right-0 top-0 z-[29]", //34px 24 - 34 = 10 / 2 = 5
"h-[100svh] w-[100vw] transition-all duration-300 md:w-[20rem]",
"outline-none",
"bg-white",
"fixed bottom-0 right-0 top-0 z-[29]",
"h-[100svh] w-[100vw] transition-all duration-500 ease-customEase md:w-[20rem]",
"bg-background outline-none",
isExpanded
? "pointer-events-auto translate-x-0 opacity-100"
: "pointer-events-none translate-x-full opacity-0",
Expand All @@ -34,19 +125,6 @@ export default function InnerWrapper({ children }: { children: React.ReactNode }
mainMenuBtnRef.current.focus();
}
}}
onTouchStart={(e) => {
setTouchStart(e.targetTouches[0].clientX);
setTouchEnd(e.targetTouches[0].clientX);
}}
onTouchMove={(e) => {
setTouchEnd(e.targetTouches[0].clientX);
}}
onTouchEnd={() => {
if (touchEnd - touchStart > 100) {
toggle();
}
setTouchStart(0);
}}
ref={sidebarRef}
>
{children}
Expand Down
136 changes: 136 additions & 0 deletions doc/feat_sidebar.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# Potential ideas for new sidebar feature

```tsx
"use client";
import { useCallback, useRef, useEffect } from "react";

import { useHeaderContext } from "@/context/header";
import { useSidebarContext } from "@/context/sidebar";
import { useMenuBtnRefContext } from "@/context/sidebar/btnRef";
import { cn } from "@/lib/utils";

export default function InnerWrapper({ children }: { children: React.ReactNode }) {
const { isAtTop } = useHeaderContext();
const { isExpanded, toggle, sidebarRef } = useSidebarContext();
const { mainMenuBtnRef } = useMenuBtnRefContext();
const touchStartXRef = useRef(0);
const animationFrameIdRef = useRef<number | null>(0); // Ref to store the animation frame ID
const touchEndXRef = useRef(0);

const directionAdjustedValue = useCallback((end: number, start: number) => {
if (end > start) {
return 100 - Math.exp(-0.04 * (end - start) + 4.6);
}
return -40 + Math.exp(-0.04 * (start - end) + 3.8);
}, []);

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;
}
if (Math.abs(touchEndXRef.current - touchStartXRef.current) > 100) {
toggle();
}
// Optional: reset transform or update based on final state
if (sidebarRef.current) {
sidebarRef.current.style.transform = "";
}
}, [sidebarRef, toggle]);

// Clean up animation frames on component unmount
useEffect(() => {
return () => {
if (animationFrameIdRef.current) {
cancelAnimationFrame(animationFrameIdRef.current);
}
};
}, [isExpanded]);
useEffect(() => {
const sidebar = sidebarRef.current;
if (!sidebar) return;

// Adding event listeners
sidebar.addEventListener("touchstart", handleTouchStart);
sidebar.addEventListener("touchmove", handleTouchMove);
sidebar.addEventListener("touchend", handleTouchEnd);

// Cleanup function
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 (
<>
<div
className={cn(
"fixed bottom-0 right-0 top-0 z-[29]",
"ease-customEase h-[100svh] w-[6.5rem] transition-all duration-500",
"bg-white outline-none",
isExpanded
? "pointer-events-auto translate-x-0 opacity-100"
: "pointer-events-none translate-x-full opacity-0"
)}
/>
<aside
className={cn(
"fixed bottom-0 right-0 top-0 z-[29]",
"ease-customEase h-[100svh] w-[100vw] transition-all duration-500 md:w-[20rem]",
"bg-white outline-none",
isExpanded
? "pointer-events-auto translate-x-0 opacity-100"
: "pointer-events-none translate-x-full opacity-0",
isAtTop ? "pt-[1.625rem]" : ""
)}
aria-label="Sidebar navigation menu"
aria-hidden={!isExpanded}
onKeyDown={(e) => {
if (e.code === "Escape" && mainMenuBtnRef.current) {
toggle();
mainMenuBtnRef.current.focus();
}
}}
// onTouchStart={handleTouchStart}
// onTouchMove={handleTouchMove}
// onTouchEnd={handleTouchEnd}
ref={sidebarRef}
>
{children}
</aside>
</>
);
}
```

The above code demonstrates sidebar functionality for assigning
and unassigning event listeners for touch-based closing.
7 changes: 7 additions & 0 deletions styles/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,8 @@ html.dark {
--page-x-spacing2: 2rem;
--header-height: 8rem;
--header-height-scrolled: 4.5rem;
--sidebar-start-x: 0px;
--sidebar-end-x: 0px;
}

@media (width >= 64rem) {
Expand Down Expand Up @@ -193,3 +195,8 @@ dialog {
dialog:-internal-dialog-in-top-layer {
@apply visible;
}

.customEase {
transition: all 500ms cubic-bezier(0.13, 0.87, 0.35, 1.09); /* custom */
transition-timing-function: cubic-bezier(0.13, 0.87, 0.35, 1.09); /* custom */
}

0 comments on commit f9d7b09

Please sign in to comment.