55 lines
1.7 KiB
TypeScript
55 lines
1.7 KiB
TypeScript
"use client";
|
|
|
|
import { useRef, useState, useEffect, type ReactNode } from "react";
|
|
|
|
interface ScrollFadeProps {
|
|
children: ReactNode;
|
|
className?: string;
|
|
/** Tailwind max-width class, e.g. "max-w-2xs". Defaults to none. */
|
|
maxWidth?: string;
|
|
}
|
|
|
|
/**
|
|
* Wraps children in a horizontally-scrollable container that shows left/right
|
|
* gradient fade indicators when there is overflowed content in that direction.
|
|
*/
|
|
export function ScrollFade({ children, className, maxWidth }: ScrollFadeProps) {
|
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
const [showLeft, setShowLeft] = useState(false);
|
|
const [showRight, setShowRight] = useState(false);
|
|
|
|
const update = () => {
|
|
const el = scrollRef.current;
|
|
if (!el) return;
|
|
setShowLeft(el.scrollLeft > 0);
|
|
setShowRight(el.scrollLeft + el.clientWidth < el.scrollWidth - 1);
|
|
};
|
|
|
|
useEffect(() => {
|
|
update();
|
|
const el = scrollRef.current;
|
|
if (!el) return;
|
|
const ro = new ResizeObserver(update);
|
|
ro.observe(el);
|
|
return () => ro.disconnect();
|
|
}, [children]);
|
|
|
|
return (
|
|
<div className={`relative ${maxWidth ?? ""}`}>
|
|
<div
|
|
ref={scrollRef}
|
|
onScroll={update}
|
|
className={`flex gap-1.5 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden ${className ?? ""}`}
|
|
>
|
|
{children}
|
|
</div>
|
|
{showLeft && (
|
|
<div className="pointer-events-none absolute inset-y-0 left-0 w-8 bg-linear-to-r from-surface to-transparent group-hover:from-surface-hover" />
|
|
)}
|
|
{showRight && (
|
|
<div className="pointer-events-none absolute inset-y-0 right-0 w-8 bg-linear-to-l from-surface to-transparent group-hover:from-surface-secondary" />
|
|
)}
|
|
</div>
|
|
);
|
|
}
|