mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-04-28 10:28:09 -04:00
99 lines
3.1 KiB
TypeScript
99 lines
3.1 KiB
TypeScript
import { ArrowLeft, ArrowRight } from '@phosphor-icons/react';
|
|
import clsx from 'clsx';
|
|
import { ReactNode, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
import { useDraggable } from 'react-use-draggable-scroll';
|
|
import { tw } from '@sd/ui';
|
|
|
|
const ArrowButton = tw.div`absolute top-1/2 z-40 flex h-8 w-8 shrink-0 -translate-y-1/2 items-center p-2 cursor-pointer justify-center rounded-full border border-app-line bg-app/50 hover:opacity-95 backdrop-blur-md transition-all duration-200`;
|
|
|
|
const HorizontalScroll = ({ children, className }: { children: ReactNode; className?: string }) => {
|
|
const ref = useRef<HTMLDivElement>(null);
|
|
const { events } = useDraggable(ref as React.MutableRefObject<HTMLDivElement>);
|
|
const [lastItemVisible, setLastItemVisible] = useState(false);
|
|
const [scroll, setScroll] = useState(0);
|
|
// If the content is overflowing, we need to show the arrows
|
|
const [isContentOverflow, setIsContentOverflow] = useState(false);
|
|
|
|
const updateScrollState = () => {
|
|
const element = ref.current;
|
|
if (element) {
|
|
setScroll(element.scrollLeft);
|
|
setLastItemVisible(element.scrollWidth - element.clientWidth === element.scrollLeft);
|
|
setIsContentOverflow(element.scrollWidth > element.clientWidth);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
const element = ref.current;
|
|
if (element) {
|
|
element.addEventListener('scroll', updateScrollState);
|
|
window.addEventListener('resize', updateScrollState);
|
|
}
|
|
return () => {
|
|
if (element) {
|
|
element.removeEventListener('scroll', updateScrollState);
|
|
window.removeEventListener('resize', updateScrollState);
|
|
}
|
|
};
|
|
}, [ref]);
|
|
|
|
useLayoutEffect(() => {
|
|
updateScrollState();
|
|
}, [ref.current?.clientWidth]);
|
|
|
|
const handleArrowOnClick = (direction: 'right' | 'left') => {
|
|
const element = ref.current;
|
|
if (!element) return;
|
|
|
|
const scrollAmount = element.clientWidth;
|
|
|
|
element.scrollTo({
|
|
left:
|
|
direction === 'left'
|
|
? element.scrollLeft + scrollAmount
|
|
: element.scrollLeft - scrollAmount,
|
|
behavior: 'smooth'
|
|
});
|
|
};
|
|
|
|
const maskImage = `linear-gradient(90deg, transparent 0.1%, rgba(0, 0, 0, 1) ${
|
|
scroll > 0 ? '10%' : '0%'
|
|
}, rgba(0, 0, 0, 1) ${lastItemVisible ? '95%' : '85%'}, transparent 99%)`;
|
|
|
|
return (
|
|
<div className={clsx(className, 'relative mb-4 flex pl-6')}>
|
|
<ArrowButton
|
|
onClick={() => handleArrowOnClick('right')}
|
|
className={clsx('left-3', scroll === 0 && 'pointer-events-none opacity-0')}
|
|
>
|
|
<ArrowLeft weight="bold" className="size-4 text-ink" />
|
|
</ArrowButton>
|
|
<div
|
|
ref={ref}
|
|
{...events}
|
|
className={clsx(
|
|
'no-scrollbar flex gap-2 space-x-px overflow-x-scroll pl-1 pr-[60px]',
|
|
isContentOverflow ? 'cursor-grab' : 'cursor-default'
|
|
)}
|
|
style={{
|
|
WebkitMaskImage: maskImage,
|
|
maskImage
|
|
}}
|
|
>
|
|
{children}
|
|
</div>
|
|
|
|
{isContentOverflow && (
|
|
<ArrowButton
|
|
onClick={() => handleArrowOnClick('left')}
|
|
className={clsx('right-3', lastItemVisible && 'pointer-events-none opacity-0')}
|
|
>
|
|
<ArrowRight weight="bold" className="size-4 text-ink" />
|
|
</ArrowButton>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default HorizontalScroll;
|