Add quick zoom button to zoom controls

This commit is contained in:
Sam Becker 2025-02-09 18:48:08 -06:00
parent 3c04ca840f
commit 33a430dcfd
3 changed files with 132 additions and 37 deletions

View File

@ -0,0 +1,64 @@
import clsx from 'clsx/lite';
import { ReactNode, RefObject, useEffect, useRef } from 'react';
import { createPortal } from 'react-dom';
import useImageZoomControls from './useImageZoomControls';
import { RiCollapseDiagonalLine, RiExpandDiagonalLine } from 'react-icons/ri';
export type ZoomControlsRef = {
open: () => void
zoom: (zoomLevel?: number) => void
}
export default function ZoomControls({
ref,
children,
isEnabled,
shouldZoomOnFKeydown,
}: {
ref?: RefObject<ZoomControlsRef | null>
children: ReactNode
isEnabled?: boolean
shouldZoomOnFKeydown?: boolean
}) {
const refContainer = useRef<HTMLDivElement>(null);
const { open, zoom, zoomLevel, isShown } = useImageZoomControls(
refContainer,
isEnabled,
shouldZoomOnFKeydown,
);
useEffect(() => {
if (ref) { ref.current = { open, zoom }; }
}, [ref, open, zoom]);
const shouldZoomTo2x = zoomLevel < 2;
const button =
<button
className={clsx(
isShown ? 'inline-flex' : 'hidden',
'fixed top-[20px] right-[70px] z-[100000]',
'size-10 items-center justify-center',
'rounded-full border-none',
'text-white bg-black/50 hover:bg-black/85',
)}
onClick={() => zoom(shouldZoomTo2x ? 2 : 1)}
>
{shouldZoomTo2x
? <RiCollapseDiagonalLine className="shrink-0" size={20} />
: <RiExpandDiagonalLine className="shrink-0" size={20} />}
</button>;
return (
<div
ref={refContainer}
className={clsx('h-full', isEnabled && 'cursor-zoom-in')}
>
{children}
{typeof window !== 'undefined'
? createPortal(button, document.body)
: button}
</div>
);
}

View File

@ -13,12 +13,36 @@ export default function useImageZoomControls(
const { setShouldRespondToKeyboardCommands } = useAppState();
const [isShown, setIsShown] = useState(false);
const [zoomLevel, setZoomLevel] = useState(1);
const [colorLight, setColorLight] = useState<string>();
useMetaThemeColor({ colorLight });
const open = useCallback(() => {
viewerRef.current?.show();
}, [viewerRef]);
const close = useCallback(() => {
viewerRef.current?.hide();
}, [viewerRef]);
const zoom = useCallback((zoomLevel = 1) => {
viewerRef.current?.zoomTo(zoomLevel);
}, [viewerRef]);
// On 'F' keydown, toggle fullscreen
const handleKeyDown = useCallback(() => {
if (shouldExpandOnFKeydown) {
viewerRef.current?.show();
}
}, [shouldExpandOnFKeydown]);
useKeydownHandler(handleKeyDown, ['F']);
useEffect(() => {
if (imageRef.current && isEnabled) {
const closeButton = document
.getElementsByClassName('viewer-close')[0] as HTMLElement;
viewerRef.current = new Viewer(imageRef.current, {
navbar: false,
title: false,
@ -34,39 +58,49 @@ export default function useImageZoomControls(
show: () => {
setShouldRespondToKeyboardCommands?.(false);
setColorLight('#000');
setIsShown(true);
if (closeButton) { closeButton.style.display = 'none'; }
},
hide: () => {
setTimeout(() => setColorLight(undefined), 300);
setTimeout(() => {
setColorLight(undefined);
setIsShown(false);
}, 300);
},
hidden: () => {
setShouldRespondToKeyboardCommands?.(true);
},
zoom: ({ detail: { ratio } }) => {
setZoomLevel(ratio);
},
view: () => {
const container = document
.getElementsByClassName('viewer-container')[0];
if (container) {
const closeButton = document
.getElementsByClassName('viewer-close')[0] as HTMLElement;
if (closeButton) { closeButton.style.display = 'inline-flex'; }
}
},
});
return () => {
viewerRef.current?.destroy();
viewerRef.current = null;
};
}
}, [imageRef, isEnabled, setShouldRespondToKeyboardCommands]);
const open = useCallback(() => {
viewerRef.current?.show();
}, [viewerRef]);
const close = useCallback(() => {
viewerRef.current?.hide();
}, [viewerRef]);
// On 'F' keydown, toggle fullscreen
const handleKeyDown = useCallback(() => {
if (shouldExpandOnFKeydown) {
viewerRef.current?.show();
}
}, [shouldExpandOnFKeydown]);
useKeydownHandler(handleKeyDown, ['F']);
}, [
imageRef,
isEnabled,
zoom,
setShouldRespondToKeyboardCommands,
]);
return {
open,
close,
zoom,
zoomLevel,
isShown,
};
}

View File

@ -36,10 +36,10 @@ import { useRef } from 'react';
import useVisible from '@/utility/useVisible';
import PhotoDate from './PhotoDate';
import { useAppState } from '@/state/AppState';
import useImageZoomControls from '@/components/image/useImageZoomControls';
import { LuExpand } from 'react-icons/lu';
import LoaderButton from '@/components/primitives/LoaderButton';
import Tooltip from '@/components/Tooltip';
import ZoomControls, { ZoomControlsRef } from '@/components/image/ZoomControls';
export default function PhotoLarge({
photo,
@ -85,7 +85,8 @@ export default function PhotoLarge({
onVisible?: () => void
}) {
const ref = useRef<HTMLDivElement>(null);
const refZoomControlsContainer = useRef<HTMLDivElement>(null);
const zoomControlsRef = useRef<ZoomControlsRef>(null);
const {
areZoomControlsShown,
@ -105,12 +106,6 @@ export default function PhotoLarge({
useVisible({ ref, onVisible });
const { open } = useImageZoomControls(
refZoomControlsContainer,
showZoomControls,
shouldZoomOnFKeydown,
);
const hasTitle =
showTitle &&
Boolean(photo.title);
@ -152,9 +147,9 @@ export default function PhotoLarge({
arePhotosMatted && 'h-[90%]',
arePhotosMatted && matteContentWidthForAspectRatio(),
)}>
<div
ref={refZoomControlsContainer}
className={clsx('h-full', showZoomControls && 'cursor-zoom-in')}
<ZoomControls
ref={zoomControlsRef}
{...{ isEnabled: showZoomControls, shouldZoomOnFKeydown }}
>
<ImageLarge
className={clsx(arePhotosMatted && 'h-full')}
@ -167,7 +162,7 @@ export default function PhotoLarge({
blurCompatibilityMode={doesPhotoNeedBlurCompatibility(photo)}
priority={priority}
/>
</div>
</ZoomControls>
</div>;
const largePhotoContainerClassName = clsx(arePhotosMatted &&
@ -297,9 +292,9 @@ export default function PhotoLarge({
// Prevent collision with admin button
!hasNonDateContent && isUserSignedIn && 'md:pr-7',
)}
// Created at is a naive datetime which
// 'createdAt' is a naive datetime which
// does not require a timezone and will not
// cause server/client time mismatch
// cause server/client time mismatches
timezone={null}
hideTime={!SHOW_TAKEN_AT_TIME}
/>
@ -311,7 +306,7 @@ export default function PhotoLarge({
<LoaderButton
title="Open Image Viewer"
icon={<LuExpand size={15} />}
onClick={open}
onClick={() => zoomControlsRef.current?.open()}
styleAs="link"
className="text-medium translate-y-[0.25px]"
hideFocusOutline
@ -322,10 +317,12 @@ export default function PhotoLarge({
photo={photo}
tag={shouldShareTag ? primaryTag : undefined}
camera={shouldShareCamera ? camera : undefined}
// eslint-disable-next-line max-len
simulation={shouldShareSimulation? photo.filmSimulation : undefined}
// eslint-disable-next-line max-len
focal={shouldShareFocalLength ? photo.focalLength : undefined}
simulation={shouldShareSimulation
? photo.filmSimulation
: undefined}
focal={shouldShareFocalLength
? photo.focalLength
: undefined}
prefetch={prefetchRelatedLinks}
/>}
{ALLOW_PUBLIC_DOWNLOADS &&