Improve masked scroll in form controls

This commit is contained in:
Sam Becker 2025-07-11 17:19:36 -05:00
parent 91562e6523
commit 781ff098b1
6 changed files with 42 additions and 27 deletions

View File

@ -161,7 +161,7 @@ export default function AdminBatchUploadActions({
readOnly={isAdding}
/>
<FieldsetFavs
className="my-6"
className="pt-2.5 pb-2"
value={formData.favorite ?? 'false'}
onChange={favorite =>
setFormData(data => ({ ...data, favorite }))}

View File

@ -9,14 +9,14 @@ export default function MaskedScroll({
setMaxSize,
hideScrollbar,
updateMaskOnEvents,
updateMaskAfterDelay,
scrollToEndOnMount,
style,
children,
...props
}: HTMLAttributes<HTMLDivElement> &
Omit<Parameters<typeof useMaskedScroll>[0], 'ref'> &
{ ref?: RefObject<HTMLDivElement | null> }) {
}: {
ref?: RefObject<HTMLDivElement | null>
} & HTMLAttributes<HTMLDivElement>
& Omit<Parameters<typeof useMaskedScroll>[0], 'ref'>) {
const refInternal = useRef<HTMLDivElement>(null);
const ref = refProp ?? refInternal;
@ -28,7 +28,6 @@ Omit<Parameters<typeof useMaskedScroll>[0], 'ref'> &
setMaxSize,
hideScrollbar,
updateMaskOnEvents,
updateMaskAfterDelay,
scrollToEndOnMount,
});

View File

@ -140,7 +140,7 @@ export default function SelectMenu({
tabIndex={tabIndex}
className={clsx(
'cursor-pointer control pl-1.5 py-2',
'flex items-center w-full h-9.5',
'flex items-center w-full h-10',
'focus:outline-2 -outline-offset-2 focus:outline-blue-600',
'select-none',
Boolean(error) && 'error',

View File

@ -1,3 +1,4 @@
import useElementHeight from '@/utility/useElementHeight';
import {
CSSProperties,
RefObject,
@ -5,6 +6,7 @@ import {
useEffect,
useMemo,
} from 'react';
import { useDebouncedCallback } from 'use-debounce';
const CSS_VAR_MASK_COLOR_START = '--mask-color-start';
const CSS_VAR_MASK_COLOR_END = '--mask-color-end';
@ -18,7 +20,6 @@ export default function useMaskedScroll({
hideScrollbar = true,
// Disable when calling 'updateMask' explicitly
updateMaskOnEvents = true,
updateMaskAfterDelay = 0,
scrollToEndOnMount,
}: {
ref: RefObject<HTMLDivElement | null>
@ -28,12 +29,13 @@ export default function useMaskedScroll({
animationDuration?: number
setMaxSize?: boolean
hideScrollbar?: boolean
updateMaskAfterDelay?: number
scrollToEndOnMount?: boolean
}) {
const isVertical = direction === 'vertical';
const updateMask = useCallback(() => {
const containerHeight = useElementHeight(containerRef);
const _updateMask = useCallback(() => {
const ref = containerRef?.current;
if (ref) {
const start = isVertical
@ -51,7 +53,9 @@ export default function useMaskedScroll({
}
}, [containerRef, isVertical]);
// Conditionally track events
const updateMask = useDebouncedCallback(_updateMask, 50, { leading: true });
// Update on scroll/resize
useEffect(() => {
const ref = containerRef?.current;
if (ref && updateMaskOnEvents) {
@ -64,18 +68,19 @@ export default function useMaskedScroll({
}
}, [containerRef, updateMask, updateMaskOnEvents]);
// Update on mount
// Update on container height change
useEffect(() => {
updateMask();
}, [updateMask]);
// Update after delay
useEffect(() => {
if (updateMaskAfterDelay) {
const timeout = setTimeout(updateMask, updateMaskAfterDelay);
return () => clearTimeout(timeout);
if (updateMaskOnEvents) {
updateMask();
}
}, [containerRef, updateMask, updateMaskAfterDelay]);
}, [containerHeight, updateMaskOnEvents, updateMask]);
// Update on mount when not responding to events
useEffect(() => {
if (!updateMaskOnEvents) {
updateMask();
}
}, [updateMask, updateMaskOnEvents]);
useEffect(() => {
const ref = containerRef?.current;

View File

@ -55,7 +55,6 @@ export default function PhotoGridPageClient({
)}
fadeSize={100}
setMaxSize={false}
updateMaskAfterDelay={500}
>
<PhotoGridSidebar {...{
...categories,

View File

@ -1,16 +1,28 @@
import { useState, RefObject, useEffect } from 'react';
import { useDebouncedCallback } from 'use-debounce';
export default function useElementHeight(
ref: RefObject<HTMLElement | null>,
shouldDebounce = true,
) {
const [height, setHeight] = useState(ref.current?.clientHeight);
const setHeightDebounced =
useDebouncedCallback(setHeight, 250, { leading: true });
useEffect(() => {
const handleResize = () => setHeight(ref.current?.clientHeight);
handleResize();
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, [ref]);
if (ref.current) {
const observer = new ResizeObserver(e => {
if (shouldDebounce) {
setHeightDebounced(e[0].contentRect.height);
} else {
setHeight(e[0].contentRect.height);
}
});
observer.observe(ref.current);
return () => observer.disconnect();
}
}, [ref, setHeightDebounced, shouldDebounce]);
return height;
}