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} readOnly={isAdding}
/> />
<FieldsetFavs <FieldsetFavs
className="my-6" className="pt-2.5 pb-2"
value={formData.favorite ?? 'false'} value={formData.favorite ?? 'false'}
onChange={favorite => onChange={favorite =>
setFormData(data => ({ ...data, favorite }))} setFormData(data => ({ ...data, favorite }))}

View File

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

View File

@ -140,7 +140,7 @@ export default function SelectMenu({
tabIndex={tabIndex} tabIndex={tabIndex}
className={clsx( className={clsx(
'cursor-pointer control pl-1.5 py-2', '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', 'focus:outline-2 -outline-offset-2 focus:outline-blue-600',
'select-none', 'select-none',
Boolean(error) && 'error', Boolean(error) && 'error',

View File

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

View File

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

View File

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