PR comment fixes - use scale approach instead
This commit is contained in:
parent
c17df709a6
commit
adb869c34a
|
@ -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.
|
||||||
|
|
Loading…
Reference in New Issue