Handle content overflow on large photos with masked component
This commit is contained in:
parent
3364c26b0c
commit
02319da5c7
@ -71,8 +71,7 @@ export default function AdminNavClient({
|
||||
'border-b border-gray-200 dark:border-gray-800',
|
||||
)}>
|
||||
<MaskedScroll
|
||||
className="grow -mx-1"
|
||||
classNameContent="flex gap-0.5 md:gap-1.5"
|
||||
className="grow -mx-1 flex gap-0.5 md:gap-1.5"
|
||||
direction="horizontal"
|
||||
>
|
||||
{items.map(({ label, href, count }) =>
|
||||
|
||||
@ -4,16 +4,14 @@ import useMaskedScroll, { MaskedScrollExternalProps } from './useMaskedScroll';
|
||||
|
||||
export default function MaskedScroll({
|
||||
direction = 'vertical',
|
||||
fadeHeight = 24,
|
||||
fadeHeight,
|
||||
hideScrollbar,
|
||||
className,
|
||||
classNameContent,
|
||||
style,
|
||||
children,
|
||||
...props
|
||||
}: HTMLAttributes<HTMLDivElement> &
|
||||
MaskedScrollExternalProps & {
|
||||
classNameContent?: string
|
||||
hideScrollbar?: boolean
|
||||
}) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
@ -22,25 +20,16 @@ MaskedScrollExternalProps & {
|
||||
|
||||
return <div
|
||||
{...props}
|
||||
ref={ref}
|
||||
className={clsx(
|
||||
direction === 'vertical'
|
||||
? 'overflow-y-hidden'
|
||||
: 'overflow-x-hidden',
|
||||
? 'max-h-full overflow-y-scroll'
|
||||
: 'max-w-full overflow-x-scroll',
|
||||
hideScrollbar && '[scrollbar-width:none]',
|
||||
className,
|
||||
)}
|
||||
style={{ maskImage, ...style }}
|
||||
>
|
||||
<div
|
||||
ref={ref}
|
||||
className={clsx(
|
||||
direction === 'vertical'
|
||||
? 'max-h-full overflow-y-auto'
|
||||
: 'max-w-full overflow-x-auto',
|
||||
hideScrollbar && '[scrollbar-width:none]',
|
||||
classNameContent,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
{children}
|
||||
</div>;
|
||||
}
|
||||
|
||||
@ -167,7 +167,7 @@ export default function CommandKClient({
|
||||
const refScroll = useRef<HTMLDivElement>(null);
|
||||
const { maskImage, updateMask } = useMaskedScroll({
|
||||
ref: refScroll,
|
||||
listenForScrollEvents: false,
|
||||
updateMaskOnEvents: false,
|
||||
});
|
||||
|
||||
// Manage action/path waiting state
|
||||
@ -207,7 +207,11 @@ export default function CommandKClient({
|
||||
|
||||
useEffect(() => {
|
||||
isOpenRef.current = isOpen;
|
||||
}, [isOpen]);
|
||||
if (isOpen) {
|
||||
const timeout = setTimeout(updateMask, 100);
|
||||
return () => clearTimeout(timeout);
|
||||
}
|
||||
}, [isOpen, updateMask]);
|
||||
|
||||
useEffect(() => {
|
||||
const down = (e: KeyboardEvent) => {
|
||||
@ -556,7 +560,10 @@ export default function CommandKClient({
|
||||
<div className="relative">
|
||||
<Command.Input
|
||||
ref={refInput}
|
||||
onChangeCapture={(e) => setQueryLive(e.currentTarget.value)}
|
||||
onChangeCapture={(e) => {
|
||||
setQueryLive(e.currentTarget.value);
|
||||
updateMask();
|
||||
}}
|
||||
className={clsx(
|
||||
'w-full min-w-0!',
|
||||
'focus:ring-0',
|
||||
@ -585,14 +592,15 @@ export default function CommandKClient({
|
||||
onScroll={updateMask}
|
||||
className={clsx(
|
||||
'overflow-y-auto',
|
||||
'mx-3 py-2',
|
||||
'mx-3 pt-2 pb-3.5',
|
||||
'[&>*>*>*]:mt-2.5',
|
||||
)}
|
||||
style={{ maxHeight, maskImage }}
|
||||
style={{ maskImage, maxHeight }}
|
||||
>
|
||||
<Command.Empty className="mt-1 pl-3 text-dim pb-4">
|
||||
{isLoading ? 'Searching ...' : 'No results found'}
|
||||
</Command.Empty>
|
||||
<div className="space-y-2.5">
|
||||
<div className="-mt-2.5">
|
||||
<Command.Empty className="mt-1 pl-3 text-dim pb-1">
|
||||
{isLoading ? 'Searching ...' : 'No results found'}
|
||||
</Command.Empty>
|
||||
{queriedSections
|
||||
.concat(categorySections)
|
||||
.concat(sectionPages)
|
||||
@ -604,7 +612,8 @@ export default function CommandKClient({
|
||||
key={heading}
|
||||
heading={<div className={clsx(
|
||||
'flex items-center',
|
||||
'px-2 pb-0.5',
|
||||
'px-2 py-1! pb-0.5',
|
||||
'text-xs font-medium text-dim tracking-wider',
|
||||
isPending && 'opacity-20',
|
||||
)}>
|
||||
{accessory &&
|
||||
@ -614,11 +623,6 @@ export default function CommandKClient({
|
||||
className={clsx(
|
||||
'uppercase',
|
||||
'select-none',
|
||||
'[&>*:first-child]:py-1',
|
||||
'[&>*:first-child]:font-medium',
|
||||
'[&>*:first-child]:text-dim',
|
||||
'[&>*:first-child]:text-xs',
|
||||
'[&>*:first-child]:tracking-wider',
|
||||
)}
|
||||
>
|
||||
{items.map(({
|
||||
@ -672,14 +676,14 @@ export default function CommandKClient({
|
||||
/>;
|
||||
})}
|
||||
</Command.Group>)}
|
||||
{footer && !queryLive &&
|
||||
<div className={clsx(
|
||||
'text-center text-base text-dim pt-1',
|
||||
'pb-2',
|
||||
)}>
|
||||
{footer}
|
||||
</div>}
|
||||
</div>
|
||||
{footer && !queryLive &&
|
||||
<div className={clsx(
|
||||
'text-center text-base text-dim pt-2 sm:pt-3',
|
||||
'pb-2.5',
|
||||
)}>
|
||||
{footer}
|
||||
</div>}
|
||||
</Command.List>
|
||||
</Modal>
|
||||
</Command.Dialog>
|
||||
|
||||
@ -10,41 +10,48 @@ export default function useMaskedScroll({
|
||||
direction = 'vertical',
|
||||
fadeHeight = 24,
|
||||
// Disable when calling 'updateMask' explicitly
|
||||
listenForScrollEvents = true,
|
||||
updateMaskOnEvents = true,
|
||||
}: MaskedScrollExternalProps & {
|
||||
ref: RefObject<HTMLDivElement | null>
|
||||
listenForScrollEvents?: boolean
|
||||
updateMaskOnEvents?: boolean
|
||||
}) {
|
||||
const [position, setPosition] = useState<'start' | 'middle' | 'end'>('start');
|
||||
const [position, setPosition] = useState({ start: true, end: true });
|
||||
|
||||
const isVertical = direction === 'vertical';
|
||||
|
||||
const updateMask = useCallback(() => {
|
||||
const ref = containerRef?.current;
|
||||
if (ref) {
|
||||
const isStart = isVertical
|
||||
const start = isVertical
|
||||
? ref.scrollTop === 0
|
||||
: ref.scrollLeft === 0;
|
||||
const isEnd = isVertical
|
||||
const end = isVertical
|
||||
? ref.scrollHeight - ref.scrollTop === ref.clientHeight
|
||||
: ref.scrollWidth - ref.scrollLeft === ref.clientWidth;
|
||||
setPosition(isStart ? 'start' : isEnd ? 'end' : 'middle');
|
||||
setPosition({ start, end });
|
||||
}
|
||||
}, [containerRef, isVertical]);
|
||||
|
||||
useEffect(() => {
|
||||
const ref = containerRef?.current;
|
||||
if (ref && listenForScrollEvents) {
|
||||
ref.addEventListener('scroll', updateMask);
|
||||
return () => ref.removeEventListener('scroll', updateMask);
|
||||
if (ref) {
|
||||
updateMask();
|
||||
if (updateMaskOnEvents) {
|
||||
ref.onscroll = updateMask;
|
||||
ref.onresize = updateMask;
|
||||
return () => {
|
||||
ref.onscroll = null;
|
||||
ref.onresize = null;
|
||||
};
|
||||
}
|
||||
}
|
||||
}, [containerRef, updateMask, listenForScrollEvents]);
|
||||
}, [containerRef, updateMask, updateMaskOnEvents]);
|
||||
|
||||
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)`;
|
||||
let mask = `linear-gradient(to ${isVertical ? 'bottom' : 'right'}, `;
|
||||
mask += 'transparent, black ';
|
||||
mask += `${!position.start ? fadeHeight : 0}px, black calc(100% - `;
|
||||
mask += `${!position.end ? fadeHeight : 0}px), transparent)`;
|
||||
return mask;
|
||||
}, [fadeHeight, isVertical, position]);
|
||||
|
||||
|
||||
@ -39,9 +39,8 @@ export default function PhotoGridPage({
|
||||
<MaskedScroll
|
||||
className={clsx(
|
||||
'sticky top-0 -mb-5 -mt-5',
|
||||
'max-h-screen h-full',
|
||||
'max-h-screen py-4',
|
||||
)}
|
||||
classNameContent="py-4"
|
||||
fadeHeight={36}
|
||||
hideScrollbar
|
||||
>
|
||||
|
||||
@ -48,6 +48,7 @@ import PhotoRecipeOverlay from '@/recipe/PhotoRecipeOverlay';
|
||||
import PhotoRecipe from '@/recipe/PhotoRecipe';
|
||||
import PhotoLens from '@/lens/PhotoLens';
|
||||
import { lensFromPhoto } from '@/lens';
|
||||
import MaskedScroll from '@/components/MaskedScroll';
|
||||
|
||||
export default function PhotoLarge({
|
||||
photo,
|
||||
@ -258,16 +259,17 @@ export default function PhotoLarge({
|
||||
</Link>}
|
||||
classNameSide="relative"
|
||||
contentSide={
|
||||
<div className="absolute inset-0">
|
||||
<DivDebugBaselineGrid className={clsx(
|
||||
'sticky top-4 self-start -translate-y-1',
|
||||
'overflow-y-scroll',
|
||||
'max-h-full',
|
||||
)}>
|
||||
<div className={clsx(
|
||||
<div className="md:absolute inset-0">
|
||||
<MaskedScroll
|
||||
className="sticky top-4 self-start"
|
||||
fadeHeight={36}
|
||||
hideScrollbar
|
||||
>
|
||||
<DivDebugBaselineGrid className={clsx(
|
||||
'-mt-1',
|
||||
'grid grid-cols-2 md:grid-cols-1',
|
||||
'gap-x-0.5 sm:gap-x-1 gap-y-baseline',
|
||||
'pb-6',
|
||||
'mb-6 md:mb-4',
|
||||
)}>
|
||||
{/* Meta */}
|
||||
<div className="pr-3 md:pr-0">
|
||||
@ -459,8 +461,8 @@ export default function PhotoLarge({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DivDebugBaselineGrid>
|
||||
</DivDebugBaselineGrid>
|
||||
</MaskedScroll>
|
||||
</div>}
|
||||
/>
|
||||
);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user