Decouple faded scroll component from behavior

This commit is contained in:
Sam Becker 2025-03-26 22:34:50 -05:00
parent fa9b62f34b
commit 29c3c7f167
3 changed files with 58 additions and 53 deletions

View File

@ -1,20 +1,15 @@
import clsx from 'clsx/lite';
import {
HTMLAttributes,
RefObject,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { HTMLAttributes, RefObject } from 'react';
import useFadedScroll from './useFadedScroll';
export default function FadedScroll({
ref: containerRef,
ref,
direction = 'vertical',
fadeHeight = 24,
hideScrollbar,
className,
classNameContent,
style,
children,
...props
}: HTMLAttributes<HTMLDivElement> & {
@ -24,52 +19,22 @@ export default function FadedScroll({
classNameContent?: string
hideScrollbar?: boolean
}) {
const contentRef = useRef<HTMLDivElement>(null);
const [position, setPosition] = useState<'start' | 'middle' | 'end'>('start');
const isVertical = direction === 'vertical';
useEffect(() => {
const ref = contentRef.current;
if (ref) {
const handleScroll = () => {
const isStart = isVertical
? ref.scrollTop === 0
: ref.scrollLeft === 0;
const isEnd = isVertical
? ref.scrollHeight - ref.scrollTop === ref.clientHeight
: ref.scrollWidth - ref.scrollLeft === ref.clientWidth;
setPosition(isStart ? 'start' : isEnd ? 'end' : 'middle');
};
ref.addEventListener('scroll', handleScroll);
return () => ref.removeEventListener('scroll', handleScroll);
}
}, [isVertical]);
const maskImage = useMemo(() => {
// eslint-disable-next-line max-len
let mask = `linear-gradient(to ${isVertical ? 'bottom' : 'right'}, transparent, black `;
mask += `${position !== 'start' ? fadeHeight : 0}px, black calc(100% - `;
mask += `${position !== 'end' ? fadeHeight : 0}px), transparent)`;
return mask;
}, [fadeHeight, isVertical, position]);
const { maskImage } = useFadedScroll(ref, direction, fadeHeight);
return <div
{...props}
ref={containerRef}
ref={ref}
className={clsx(
isVertical
direction === 'vertical'
? 'overflow-y-hidden'
: 'overflow-x-hidden',
className,
)}
style={{ maskImage }}
style={{ maskImage, ...style }}
>
<div
ref={contentRef}
className={clsx(
isVertical
direction === 'vertical'
? 'max-h-full overflow-y-auto'
: 'max-w-full overflow-x-auto',
hideScrollbar && '[scrollbar-width:none]',

View File

@ -155,7 +155,7 @@ export default function CommandKClient({
const ref = useRef<HTMLInputElement>(null);
const mobileViewportHeight = useVisualViewportHeight();
const heightMinimum = '20rem';
const heightMinimum = '18rem';
const maxHeight = useMemo(() => {
const positionY = ref.current?.getBoundingClientRect().y;
return mobileViewportHeight && positionY
@ -573,14 +573,16 @@ export default function CommandKClient({
</span>}
</div>
</div>
<Command.List className={clsx(
'relative overflow-y-auto',
'mx-3 pt-3',
)} style={{
maxHeight,
// eslint-disable-next-line max-len
maskImage: 'linear-gradient(to bottom, transparent, black 20px, black calc(100% - 20px), transparent)',
}}>
<Command.List
className={clsx(
'overflow-y-auto',
'mx-3 pt-3',
)} style={{
maxHeight,
// eslint-disable-next-line max-len
maskImage: 'linear-gradient(to bottom, transparent, black 20px, black calc(100% - 20px), transparent)',
}}
>
<div className="pb-1 md:pb-2">
<Command.Empty className="mt-1 pl-3 text-dim pb-4">
{isLoading ? 'Searching ...' : 'No results found'}

View File

@ -0,0 +1,38 @@
import { RefObject, useEffect, useMemo, useState } from 'react';
export default function useFadedScroll(
containerRef?: RefObject<HTMLDivElement | null>,
direction: 'vertical' | 'horizontal' = 'vertical',
fadeHeight = 24,
) {
const [position, setPosition] = useState<'start' | 'middle' | 'end'>('start');
const isVertical = direction === 'vertical';
const ref = containerRef?.current?.children[0] as HTMLElement;
useEffect(() => {
if (ref) {
const handleScroll = () => {
const isStart = isVertical
? ref.scrollTop === 0
: ref.scrollLeft === 0;
const isEnd = isVertical
? ref.scrollHeight - ref.scrollTop === ref.clientHeight
: ref.scrollWidth - ref.scrollLeft === ref.clientWidth;
setPosition(isStart ? 'start' : isEnd ? 'end' : 'middle');
};
ref.addEventListener('scroll', handleScroll);
return () => ref.removeEventListener('scroll', handleScroll);
}
}, [ref, isVertical]);
const maskImage = useMemo(() => {
// eslint-disable-next-line max-len
let mask = `linear-gradient(to ${isVertical ? 'bottom' : 'right'}, transparent, black `;
mask += `${position !== 'start' ? fadeHeight : 0}px, black calc(100% - `;
mask += `${position !== 'end' ? fadeHeight : 0}px), transparent)`;
return mask;
}, [fadeHeight, isVertical, position]);
return { maskImage };
}