Refine useMaskedScroll API

This commit is contained in:
Sam Becker 2025-04-30 18:19:42 -05:00
parent 5f9a340a25
commit 7518a88d41
3 changed files with 37 additions and 35 deletions

View File

@ -164,7 +164,7 @@ export default function CommandKClient({
}, [mobileViewportHeight]); }, [mobileViewportHeight]);
const refScroll = useRef<HTMLDivElement>(null); const refScroll = useRef<HTMLDivElement>(null);
const { maskStyle, updateMask } = useMaskedScroll({ const { styleMask, updateMask } = useMaskedScroll({
ref: refScroll, ref: refScroll,
updateMaskOnEvents: false, updateMaskOnEvents: false,
fadeSize: 50, fadeSize: 50,
@ -592,7 +592,7 @@ export default function CommandKClient({
'mx-3 pt-2 pb-3.5', 'mx-3 pt-2 pb-3.5',
'[&>*>*>*]:mt-2.5', '[&>*>*>*]:mt-2.5',
)} )}
style={{ ...maskStyle, maxHeight }} style={{ ...styleMask, maxHeight }}
> >
<div className="-mt-2.5"> <div className="-mt-2.5">
<Command.Empty className="mt-1 pl-3 text-dim pb-1"> <Command.Empty className="mt-1 pl-3 text-dim pb-1">

View File

@ -1,26 +1,29 @@
import clsx from 'clsx/lite'; import clsx from 'clsx/lite';
import { HTMLAttributes, useRef } from 'react'; import { HTMLAttributes, useRef } from 'react';
import useMaskedScroll, { MaskedScrollExternalProps } from './useMaskedScroll'; import useMaskedScroll from './useMaskedScroll';
export default function MaskedScroll({ export default function MaskedScroll({
direction = 'vertical', direction = 'vertical',
fadeSize, fadeSize,
scrollToEndOnMount, animationDuration,
hideScrollbar, hideScrollbar,
updateMaskOnEvents,
scrollToEndOnMount,
className, className,
style, style,
children, children,
...props ...props
}: HTMLAttributes<HTMLDivElement> & }: HTMLAttributes<HTMLDivElement> &
MaskedScrollExternalProps & { Parameters<typeof useMaskedScroll>[0]) {
hideScrollbar?: boolean
}) {
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const { maskStyle } = useMaskedScroll({ const { styleMask } = useMaskedScroll({
ref, ref,
direction, direction,
fadeSize, fadeSize,
animationDuration,
hideScrollbar,
updateMaskOnEvents,
scrollToEndOnMount, scrollToEndOnMount,
}); });
@ -31,10 +34,9 @@ MaskedScrollExternalProps & {
direction === 'vertical' direction === 'vertical'
? 'max-h-full overflow-y-scroll' ? 'max-h-full overflow-y-scroll'
: 'max-w-full overflow-x-scroll', : 'max-w-full overflow-x-scroll',
hideScrollbar && '[scrollbar-width:none]',
className, className,
)} )}
style={{ ...maskStyle, ...style }} style={{ ...styleMask, ...style }}
> >
{children} {children}
</div>; </div>;

View File

@ -6,27 +6,26 @@ import {
useMemo, useMemo,
} from 'react'; } from 'react';
const CSS_VAR_START = '--mask-start'; const CSS_VAR_MASK_COLOR_START = '--mask-color-start';
const CSS_VAR_END = '--mask-end'; const CSS_VAR_MASK_COLOR_END = '--mask-color-end';
export interface MaskedScrollExternalProps {
direction?: 'vertical' | 'horizontal'
fadeSize?: number
animationDuration?: number
scrollToEndOnMount?: boolean
}
export default function useMaskedScroll({ export default function useMaskedScroll({
ref: containerRef, ref: containerRef,
direction = 'vertical', direction = 'vertical',
fadeSize = 24, fadeSize = 24,
animationDuration = 0.3, animationDuration = 0.3,
hideScrollbar,
// Disable when calling 'updateMask' explicitly // Disable when calling 'updateMask' explicitly
updateMaskOnEvents = true, updateMaskOnEvents = true,
scrollToEndOnMount, scrollToEndOnMount,
}: MaskedScrollExternalProps & { }: {
ref: RefObject<HTMLDivElement | null> ref: RefObject<HTMLDivElement | null>
updateMaskOnEvents?: boolean updateMaskOnEvents?: boolean
direction?: 'vertical' | 'horizontal'
fadeSize?: number
animationDuration?: number
hideScrollbar?: boolean
scrollToEndOnMount?: boolean
}) { }) {
const isVertical = direction === 'vertical'; const isVertical = direction === 'vertical';
@ -39,12 +38,12 @@ export default function useMaskedScroll({
const end = isVertical const end = isVertical
? (ref.scrollHeight - ref.scrollTop) - ref.clientHeight < 1 ? (ref.scrollHeight - ref.scrollTop) - ref.clientHeight < 1
: (ref.scrollWidth - ref.scrollLeft) - ref.clientWidth < 1; : (ref.scrollWidth - ref.scrollLeft) - ref.clientWidth < 1;
ref.style.setProperty(CSS_VAR_START, !start ref.style.setProperty(CSS_VAR_MASK_COLOR_START, start
? 'rgba(0, 0, 0, 0)' ? 'rgba(0, 0, 0, 1)'
: 'rgba(0, 0, 0, 1)'); : 'rgba(0, 0, 0, 0)');
ref.style.setProperty(CSS_VAR_END, !end ref.style.setProperty(CSS_VAR_MASK_COLOR_END, end
? 'rgba(0, 0, 0, 0)' ? 'rgba(0, 0, 0, 1)'
: 'rgba(0, 0, 0, 1)'); : 'rgba(0, 0, 0, 0)');
} }
}, [containerRef, isVertical]); }, [containerRef, isVertical]);
@ -76,13 +75,13 @@ export default function useMaskedScroll({
useEffect(() => { useEffect(() => {
try { try {
window.CSS.registerProperty({ window.CSS.registerProperty({
name: CSS_VAR_START, name: CSS_VAR_MASK_COLOR_START,
syntax: '<color>', syntax: '<color>',
initialValue: 'rgba(0, 0, 0, 0)', initialValue: 'rgba(0, 0, 0, 0)',
inherits: false, inherits: false,
}); });
window.CSS.registerProperty({ window.CSS.registerProperty({
name: CSS_VAR_END, name: CSS_VAR_MASK_COLOR_END,
syntax: '<color>', syntax: '<color>',
initialValue: 'rgba(0, 0, 0, 0)', initialValue: 'rgba(0, 0, 0, 0)',
inherits: false, inherits: false,
@ -90,23 +89,24 @@ export default function useMaskedScroll({
} catch {} } catch {}
}, []); }, []);
const maskStyle: CSSProperties = useMemo(() => { const styleMask: CSSProperties = useMemo(() => {
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
const gradientStart = `linear-gradient(to ${isVertical ? 'bottom' : 'right'}, var(${CSS_VAR_START}), black ${fadeSize}px)`; const gradientStart = `linear-gradient(to ${isVertical ? 'bottom' : 'right'}, var(${CSS_VAR_MASK_COLOR_START}), black ${fadeSize}px)`;
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
const gradientEnd = `linear-gradient(to ${isVertical ? 'top' : 'left'}, var(${CSS_VAR_END}), black ${fadeSize}px)`; const gradientEnd = `linear-gradient(to ${isVertical ? 'top' : 'left'}, var(${CSS_VAR_MASK_COLOR_END}), black ${fadeSize}px)`;
const maskImage = [gradientStart, gradientEnd].join(', '); const maskImage = [gradientStart, gradientEnd].join(', ');
const transition = [ const transition = [
`${CSS_VAR_START} ${animationDuration}s ease-in-out`, `${CSS_VAR_MASK_COLOR_START} ${animationDuration}s ease-in-out`,
`${CSS_VAR_END} ${animationDuration}s ease-in-out`, `${CSS_VAR_MASK_COLOR_END} ${animationDuration}s ease-in-out`,
].join(', '); ].join(', ');
return { return {
maskImage, maskImage,
maskComposite: 'intersect', maskComposite: 'intersect',
maskRepeat: 'no-repeat', maskRepeat: 'no-repeat',
transition, transition,
...hideScrollbar && { scrollbarWidth: 'none' },
}; };
}, [isVertical, fadeSize, animationDuration]); }, [isVertical, fadeSize, animationDuration, hideScrollbar]);
return { maskStyle, updateMask }; return { styleMask, updateMask };
} }