PR comment fixes - use scale approach instead

This commit is contained in:
jongomez 2023-09-27 09:08:27 +01:00 committed by Jon
parent c17df709a6
commit adb869c34a
1 changed files with 119 additions and 87 deletions

View File

@ -3,20 +3,90 @@ import { lsdUtils } from '@/utils/lsd.utils'
import { useIsMobile, useOnWindowResize } from '@/utils/ui.utils' import { useIsMobile, useOnWindowResize } from '@/utils/ui.utils'
import { IconButton } from '@acid-info/lsd-react' import { IconButton } from '@acid-info/lsd-react'
import styled from '@emotion/styled' import styled from '@emotion/styled'
import React, { import React, { useCallback, useEffect, useRef, useState } from 'react'
CSSProperties,
useCallback,
useEffect,
useRef,
useState,
} from 'react'
import QuickPinchZoom, { make3dTransformValue } from 'react-quick-pinch-zoom' import QuickPinchZoom, { make3dTransformValue } from 'react-quick-pinch-zoom'
import { useWindowScroll } from 'react-use' import { useWindowScroll } from 'react-use'
import { ExitFullscreenIcon } from '../Icons/ExitFullscreenIcon' import { ExitFullscreenIcon } from '../Icons/ExitFullscreenIcon'
import { FullscreenIcon } from '../Icons/FullscreenIcon' import { FullscreenIcon } from '../Icons/FullscreenIcon'
type ExpandedTransformValues = {
scale: number
translateX: number
translateY: number
windowCenterX: number
windowCenterY: number
originalWidth: number
originalHeight: number
}
const DEFAULT_TRANSFORM_VALUES: ExpandedTransformValues = {
scale: 1,
translateX: 0,
translateY: 0,
windowCenterX: 0,
windowCenterY: 0,
originalWidth: 0,
originalHeight: 0,
}
// Places the caption below the media.
const getCaptionPositionStyles = (
captionElement: HTMLElement | null,
expandedTransformValues: ExpandedTransformValues,
): React.CSSProperties => {
if (!captionElement) return {}
const { scale, originalWidth, originalHeight, windowCenterX, windowCenterY } =
expandedTransformValues
// Calculate the caption position - it should be below the media.
// Here, we assume the media is centered in the window. So, the expanded bottom position is:
const expandedBottom = windowCenterY + (originalHeight * scale) / 2
const expandedLeft = windowCenterX - (originalWidth * scale) / 2
return {
position: 'fixed',
top: expandedBottom,
left: expandedLeft,
width: originalWidth * scale,
}
}
const calculateExpandedTransformValues = (
element: HTMLElement | null,
isMobile: boolean,
): ExpandedTransformValues => {
if (!element) return DEFAULT_TRANSFORM_VALUES
const vw = document.body.clientWidth
const vh = window.innerHeight
const maxWidth = isMobile ? vw - 32 : vw * 0.9375
const maxHeight = vh - 160
const rect = element.getBoundingClientRect()
const scale = Math.min(maxHeight / rect.height, maxWidth / rect.width)
const center = [rect.left + rect.width / 2, rect.top + rect.height / 2]
const windowCenter = [vw / 2, vh / 2]
const translate = windowCenter.map((w, i) => (w - center[i]!) / scale)
return {
scale,
translateX: translate[0],
translateY: translate[1],
originalWidth: rect.width,
originalHeight: rect.height,
windowCenterX: windowCenter[0],
windowCenterY: windowCenter[1],
}
}
type UseLightBoxReturnType = { type UseLightBoxReturnType = {
getDisplayedStyle: (element: HTMLElement | null) => React.CSSProperties expandedCSS: React.CSSProperties
expandedTransformValues: ExpandedTransformValues
close: () => void close: () => void
display: (element: HTMLElement) => void display: (element: HTMLElement) => void
isDisplayedElement: (el: HTMLElement) => boolean isDisplayedElement: (el: HTMLElement) => boolean
@ -31,54 +101,18 @@ export const useLightBox = (): UseLightBoxReturnType => {
const [displayedElement, setDisplayedElement] = useState<HTMLElement | null>( const [displayedElement, setDisplayedElement] = useState<HTMLElement | null>(
null, null,
) )
const [displayedStyle, setDisplayedStyle] = useState<CSSProperties>({
opacity: '0.5',
})
const isMobile = useIsMobile() const isMobile = useIsMobile()
const [expandedTransformValues, setExpandedTransformValues] =
const defaultStyle: CSSProperties = { useState<ExpandedTransformValues>(DEFAULT_TRANSFORM_VALUES)
opacity: 1, const { scale, translateX, translateY } = expandedTransformValues
transform: 'translate(0px, 0px)', const isActive = !!displayedElement
width: '100%',
height: '100%',
}
const display = (element: HTMLElement) => { const display = (element: HTMLElement) => {
setDisplayedElement(element) setDisplayedElement(element)
const vw = document.body.clientWidth const transformValues = calculateExpandedTransformValues(element, isMobile)
const vh = window.innerHeight
const maxWidth = isMobile ? vw - 32 : vw * 0.9375 setExpandedTransformValues(transformValues)
const maxHeight = vh - 160
const rect = element.getBoundingClientRect()
// Calculate the scaling factor for both dimensions
const widthScale = maxWidth / rect.width
const heightScale = maxHeight / rect.height
// Pick the smaller scale factor to ensure the image fits within maxWidth and maxHeight
const scale = Math.min(widthScale, heightScale)
// Calculate the new dimensions of the image
const newWidth = rect.width * scale
const newHeight = rect.height * scale
// Compute the translation to center the image
const centerX = (vw - newWidth) / 2
const centerY = (vh - newHeight) / 2
const translateX = centerX - rect.left
const translateY = centerY - rect.top
setDisplayedStyle({
zIndex: 202,
width: `${newWidth}px`,
height: `${newHeight}px`,
transform: `translate(${translateX}px, ${translateY}px)`,
position: 'relative',
})
} }
const close = useCallback(() => { const close = useCallback(() => {
@ -96,21 +130,27 @@ export const useLightBox = (): UseLightBoxReturnType => {
if (displayedElement) close() if (displayedElement) close()
}, [scrollY]) }, [scrollY])
// Only for mobile - toggle the whole page's overflow when the lightbox is displayed. // Only for mobile - hide the whole page's overflow when the lightbox is displayed.
useEffect(() => { useEffect(() => {
const html = document.querySelector('html')! const html = document.querySelector('html')!
html.style.overflow = isMobile && displayedElement ? 'hidden' : 'initial' html.style.overflow = isMobile && displayedElement ? 'hidden' : 'initial'
}, [isMobile, displayedElement]) }, [isMobile, displayedElement])
const expandedCSS: React.CSSProperties = isActive
? {
zIndex: 203,
transform: `scale(${scale}) translate(${translateX}px, ${translateY}px)`,
position: 'relative',
}
: {}
return { return {
getDisplayedStyle: (element: HTMLElement | null) => ({ expandedCSS,
...defaultStyle, expandedTransformValues,
...(element === displayedElement ? displayedStyle : {}),
}),
close, close,
display, display,
isDisplayedElement: (el: HTMLElement) => displayedElement === el, isDisplayedElement: (el: HTMLElement) => displayedElement === el,
isActive: !!displayedElement, isActive,
isMobile, isMobile,
} }
} }
@ -128,7 +168,8 @@ type LightBoxProps = {
export const LightBox = ({ children, caption }: LightBoxProps) => { export const LightBox = ({ children, caption }: LightBoxProps) => {
const { const {
getDisplayedStyle, expandedCSS,
expandedTransformValues,
display, display,
isDisplayedElement, isDisplayedElement,
isActive, isActive,
@ -136,25 +177,15 @@ export const LightBox = ({ children, caption }: LightBoxProps) => {
isMobile, isMobile,
} = useLightBox() } = useLightBox()
const ref = useRef<HTMLDivElement>(null) const ref = useRef<HTMLDivElement>(null)
const childRef = useRef<HTMLDivElement>(null) const pinchZoomChildRef = useRef<HTMLDivElement>(null)
const captionRef = useRef<HTMLElement>(null) const captionRef = useRef<HTMLElement>(null)
const displayedStyle = getDisplayedStyle(ref.current)
const handleUpdate = useCallback(({ x, y, scale }: OnUpdateParams) => { const handleUpdate = useCallback(({ x, y, scale }: OnUpdateParams) => {
const img = childRef.current const img = pinchZoomChildRef.current
if (img) { if (img) {
const transformValue = make3dTransformValue({ x, y, scale }) const transformValue = make3dTransformValue({ x, y, scale })
img.style.transform = transformValue img.style.transform = transformValue
} }
// Hide / show the captions when pinch zooming.
if (captionRef.current && scale > 1) {
captionRef.current.style.opacity = '0'
}
if (captionRef.current && scale <= 1) {
captionRef.current.style.opacity = '1'
}
}, []) }, [])
const lightBoxContent = const lightBoxContent =
@ -164,7 +195,7 @@ export const LightBox = ({ children, caption }: LightBoxProps) => {
doubleTapZoomOutOnMaxScale doubleTapZoomOutOnMaxScale
maxZoom={3} maxZoom={3}
> >
<div ref={childRef}>{children}</div> <div ref={pinchZoomChildRef}>{children}</div>
</QuickPinchZoom> </QuickPinchZoom>
) : ( ) : (
<> <>
@ -180,12 +211,6 @@ export const LightBox = ({ children, caption }: LightBoxProps) => {
</> </>
) )
// Edge case: if the user pinches to zoom in, and presses the exit fullscreen button,
// the opacity of the caption will be set to 0. This will reset it to 1.
if (captionRef.current) {
captionRef.current.style.opacity = '1'
}
return ( return (
<> <>
{isActive && ( {isActive && (
@ -201,20 +226,17 @@ export const LightBox = ({ children, caption }: LightBoxProps) => {
</> </>
)} )}
<LightboxMediaContainer <LightboxMediaContainer ref={ref} style={expandedCSS} isActive={isActive}>
ref={ref}
style={displayedStyle}
isActive={isActive}
>
{lightBoxContent} {lightBoxContent}
</LightboxMediaContainer> </LightboxMediaContainer>
<LightBoxCaption <LightBoxCaption
style={
isActive
? getCaptionPositionStyles(ref.current, expandedTransformValues)
: undefined
}
isActive={isActive} isActive={isActive}
style={{
transform: displayedStyle.transform,
width: displayedStyle.width,
}}
ref={captionRef} ref={captionRef}
> >
{caption} {caption}
@ -226,7 +248,15 @@ export const LightBox = ({ children, caption }: LightBoxProps) => {
const LightBoxCaption = styled.figcaption<{ isActive?: boolean }>` const LightBoxCaption = styled.figcaption<{ isActive?: boolean }>`
padding-top: 8px; padding-top: 8px;
${lsdUtils.typography('body3')} ${lsdUtils.typography('body3')}
transition: all 0.3s ease-in-out;
@keyframes fadeIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
${(props) => ${(props) =>
props.isActive && props.isActive &&
@ -234,9 +264,10 @@ const LightBoxCaption = styled.figcaption<{ isActive?: boolean }>`
${lsdUtils.typography('body1')} ${lsdUtils.typography('body1')}
z-index: 202; z-index: 202;
// The following will prevent very large captions from overflowing / being cut off. // The following prevents very large captions from overflowing.
height: 72px; height: 72px;
overflow: auto; overflow: auto;
animation: fadeIn 0.4s forwards; // Apply the fadeIn animation when active
`} `}
` `
@ -271,6 +302,7 @@ const ExitFullscreenIconButton = styled(IconButton)`
` `
const LightboxMediaContainer = styled.div<{ isActive?: boolean }>` const LightboxMediaContainer = styled.div<{ isActive?: boolean }>`
position: relative;
transition: all 0.3s ease-in-out; transition: all 0.3s ease-in-out;
// Show the fullscreen button when users hover over the container. // Show the fullscreen button when users hover over the container.