diff --git a/src/components/image/ZoomControls.tsx b/src/components/image/ZoomControls.tsx new file mode 100644 index 00000000..1d465167 --- /dev/null +++ b/src/components/image/ZoomControls.tsx @@ -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 + children: ReactNode + isEnabled?: boolean + shouldZoomOnFKeydown?: boolean +}) { + const refContainer = useRef(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 = + ; + + return ( +
+ {children} + {typeof window !== 'undefined' + ? createPortal(button, document.body) + : button} +
+ ); +} diff --git a/src/components/image/useImageZoomControls.ts b/src/components/image/useImageZoomControls.ts index d7b5c584..40383679 100644 --- a/src/components/image/useImageZoomControls.ts +++ b/src/components/image/useImageZoomControls.ts @@ -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(); 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, }; } diff --git a/src/photo/PhotoLarge.tsx b/src/photo/PhotoLarge.tsx index 94125524..f5754fa5 100644 --- a/src/photo/PhotoLarge.tsx +++ b/src/photo/PhotoLarge.tsx @@ -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(null); - const refZoomControlsContainer = useRef(null); + + const zoomControlsRef = useRef(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(), )}> -
-
+ ; 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({ } - 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 &&