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 { IconButton } from '@acid-info/lsd-react'
import styled from '@emotion/styled'
import React, {
CSSProperties,
useCallback,
useEffect,
useRef,
useState,
} from 'react'
import React, { useCallback, useEffect, useRef, useState } from 'react'
import QuickPinchZoom, { make3dTransformValue } from 'react-quick-pinch-zoom'
import { useWindowScroll } from 'react-use'
import { ExitFullscreenIcon } from '../Icons/ExitFullscreenIcon'
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 = {
getDisplayedStyle: (element: HTMLElement | null) => React.CSSProperties
expandedCSS: React.CSSProperties
expandedTransformValues: ExpandedTransformValues
close: () => void
display: (element: HTMLElement) => void
isDisplayedElement: (el: HTMLElement) => boolean
@ -31,54 +101,18 @@ export const useLightBox = (): UseLightBoxReturnType => {
const [displayedElement, setDisplayedElement] = useState<HTMLElement | null>(
null,
)
const [displayedStyle, setDisplayedStyle] = useState<CSSProperties>({
opacity: '0.5',
})
const isMobile = useIsMobile()
const defaultStyle: CSSProperties = {
opacity: 1,
transform: 'translate(0px, 0px)',
width: '100%',
height: '100%',
}
const [expandedTransformValues, setExpandedTransformValues] =
useState<ExpandedTransformValues>(DEFAULT_TRANSFORM_VALUES)
const { scale, translateX, translateY } = expandedTransformValues
const isActive = !!displayedElement
const display = (element: HTMLElement) => {
setDisplayedElement(element)
const vw = document.body.clientWidth
const vh = window.innerHeight
const transformValues = calculateExpandedTransformValues(element, isMobile)
const maxWidth = isMobile ? vw - 32 : vw * 0.9375
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',
})
setExpandedTransformValues(transformValues)
}
const close = useCallback(() => {
@ -96,21 +130,27 @@ export const useLightBox = (): UseLightBoxReturnType => {
if (displayedElement) close()
}, [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(() => {
const html = document.querySelector('html')!
html.style.overflow = isMobile && displayedElement ? 'hidden' : 'initial'
}, [isMobile, displayedElement])
const expandedCSS: React.CSSProperties = isActive
? {
zIndex: 203,
transform: `scale(${scale}) translate(${translateX}px, ${translateY}px)`,
position: 'relative',
}
: {}
return {
getDisplayedStyle: (element: HTMLElement | null) => ({
...defaultStyle,
...(element === displayedElement ? displayedStyle : {}),
}),
expandedCSS,
expandedTransformValues,
close,
display,
isDisplayedElement: (el: HTMLElement) => displayedElement === el,
isActive: !!displayedElement,
isActive,
isMobile,
}
}
@ -128,7 +168,8 @@ type LightBoxProps = {
export const LightBox = ({ children, caption }: LightBoxProps) => {
const {
getDisplayedStyle,
expandedCSS,
expandedTransformValues,
display,
isDisplayedElement,
isActive,
@ -136,25 +177,15 @@ export const LightBox = ({ children, caption }: LightBoxProps) => {
isMobile,
} = useLightBox()
const ref = useRef<HTMLDivElement>(null)
const childRef = useRef<HTMLDivElement>(null)
const pinchZoomChildRef = useRef<HTMLDivElement>(null)
const captionRef = useRef<HTMLElement>(null)
const displayedStyle = getDisplayedStyle(ref.current)
const handleUpdate = useCallback(({ x, y, scale }: OnUpdateParams) => {
const img = childRef.current
const img = pinchZoomChildRef.current
if (img) {
const transformValue = make3dTransformValue({ x, y, scale })
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 =
@ -164,7 +195,7 @@ export const LightBox = ({ children, caption }: LightBoxProps) => {
doubleTapZoomOutOnMaxScale
maxZoom={3}
>
<div ref={childRef}>{children}</div>
<div ref={pinchZoomChildRef}>{children}</div>
</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 (
<>
{isActive && (
@ -201,20 +226,17 @@ export const LightBox = ({ children, caption }: LightBoxProps) => {
</>
)}
<LightboxMediaContainer
ref={ref}
style={displayedStyle}
isActive={isActive}
>
<LightboxMediaContainer ref={ref} style={expandedCSS} isActive={isActive}>
{lightBoxContent}
</LightboxMediaContainer>
<LightBoxCaption
style={
isActive
? getCaptionPositionStyles(ref.current, expandedTransformValues)
: undefined
}
isActive={isActive}
style={{
transform: displayedStyle.transform,
width: displayedStyle.width,
}}
ref={captionRef}
>
{caption}
@ -226,7 +248,15 @@ export const LightBox = ({ children, caption }: LightBoxProps) => {
const LightBoxCaption = styled.figcaption<{ isActive?: boolean }>`
padding-top: 8px;
${lsdUtils.typography('body3')}
transition: all 0.3s ease-in-out;
@keyframes fadeIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
${(props) =>
props.isActive &&
@ -234,9 +264,10 @@ const LightBoxCaption = styled.figcaption<{ isActive?: boolean }>`
${lsdUtils.typography('body1')}
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;
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 }>`
position: relative;
transition: all 0.3s ease-in-out;
// Show the fullscreen button when users hover over the container.