fix(lightbox): anchor save hint below rendered image
Measure the image's rendered bottom edge with refs + ResizeObserver and position the long-press save hint relative to it instead of pinning to screen center or stage bottom. Enlarges the toast for mobile legibility and clamps the offset so tall portrait images don't push it offscreen.
This commit is contained in:
@@ -152,7 +152,38 @@ function LightboxView({
|
|||||||
const [index, setIndex] = useState(startIndex);
|
const [index, setIndex] = useState(startIndex);
|
||||||
const [isCaptionVisible, setIsCaptionVisible] = useState(true);
|
const [isCaptionVisible, setIsCaptionVisible] = useState(true);
|
||||||
const [showSaveHint, setShowSaveHint] = useState(true);
|
const [showSaveHint, setShowSaveHint] = useState(true);
|
||||||
|
const [hintTop, setHintTop] = useState<number | null>(null);
|
||||||
const touchStartX = useRef<number | null>(null);
|
const touchStartX = useRef<number | null>(null);
|
||||||
|
const stageRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const imgRef = useRef<HTMLImageElement | null>(null);
|
||||||
|
|
||||||
|
const measureHintPosition = useCallback(() => {
|
||||||
|
const stage = stageRef.current;
|
||||||
|
const img = imgRef.current;
|
||||||
|
if (!stage || !img) return;
|
||||||
|
const stageRect = stage.getBoundingClientRect();
|
||||||
|
const imgRect = img.getBoundingClientRect();
|
||||||
|
const gap = 12;
|
||||||
|
const safeBottomReserve = 80;
|
||||||
|
const desired = imgRect.bottom - stageRect.top + gap;
|
||||||
|
const maxTop = stageRect.height - safeBottomReserve;
|
||||||
|
setHintTop(Math.max(0, Math.min(desired, maxTop)));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!showSaveHint) return;
|
||||||
|
measureHintPosition();
|
||||||
|
const img = imgRef.current;
|
||||||
|
const stage = stageRef.current;
|
||||||
|
const ro = new ResizeObserver(() => measureHintPosition());
|
||||||
|
if (img) ro.observe(img);
|
||||||
|
if (stage) ro.observe(stage);
|
||||||
|
window.addEventListener("resize", measureHintPosition);
|
||||||
|
return () => {
|
||||||
|
ro.disconnect();
|
||||||
|
window.removeEventListener("resize", measureHintPosition);
|
||||||
|
};
|
||||||
|
}, [measureHintPosition, showSaveHint, index]);
|
||||||
|
|
||||||
// Clamp at the ends instead of wrapping; the nav arrows / swipe / arrow
|
// Clamp at the ends instead of wrapping; the nav arrows / swipe / arrow
|
||||||
// keys should all behave like a linear gallery, not a carousel.
|
// keys should all behave like a linear gallery, not a carousel.
|
||||||
@@ -241,7 +272,10 @@ function LightboxView({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Image stage */}
|
{/* Image stage */}
|
||||||
<div className="relative flex min-h-0 w-full flex-1 items-center justify-center">
|
<div
|
||||||
|
ref={stageRef}
|
||||||
|
className="relative flex min-h-0 w-full flex-1 items-center justify-center"
|
||||||
|
>
|
||||||
{hasMany && index > 0 ? (
|
{hasMany && index > 0 ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -270,25 +304,26 @@ function LightboxView({
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{showSaveHint ? (
|
{showSaveHint ? (
|
||||||
<div className="pointer-events-none fixed inset-x-0 top-[58%] z-30 flex -translate-y-1/2 justify-center px-4">
|
<div
|
||||||
|
className="pointer-events-none absolute inset-x-0 z-30 flex justify-center px-4"
|
||||||
|
style={hintTop != null ? { top: hintTop } : { bottom: 16 }}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className="pointer-events-auto relative flex w-44 max-w-[calc(100vw-2rem)] flex-col items-center gap-1.5 rounded-xl bg-black/70 px-4 py-3 text-center text-white shadow-2xl ring-1 ring-white/20 backdrop-blur-md animate-scale-in"
|
className="pointer-events-auto relative flex max-w-[calc(100vw-2rem)] items-center gap-3 rounded-full bg-black/75 px-5 py-3 text-white shadow-2xl ring-1 ring-white/20 backdrop-blur-md animate-scale-in"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
|
<span className="text-2xl leading-none animate-bounce">☝️</span>
|
||||||
|
<span className="text-sm font-semibold leading-5">
|
||||||
|
{t("longPressImageSave")}
|
||||||
|
</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowSaveHint(false)}
|
onClick={() => setShowSaveHint(false)}
|
||||||
className="absolute right-1.5 top-1.5 flex h-5 w-5 items-center justify-center rounded-full bg-white/10 text-white/80 transition hover:bg-white/20 hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/70"
|
className="flex h-7 w-7 items-center justify-center rounded-full bg-white/10 text-white/80 transition hover:bg-white/20 hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/70"
|
||||||
aria-label="Close hint"
|
aria-label="Close hint"
|
||||||
>
|
>
|
||||||
<X className="h-3.5 w-3.5" />
|
<X className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-white/15 text-2xl animate-bounce">
|
|
||||||
☝️
|
|
||||||
</div>
|
|
||||||
<div className="max-w-full break-words text-xs font-medium leading-4">
|
|
||||||
{t("longPressImageSave")}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -315,8 +350,10 @@ function LightboxView({
|
|||||||
{/* No select-none / touch-callout:none here so iOS Safari's native
|
{/* No select-none / touch-callout:none here so iOS Safari's native
|
||||||
long-press menu ("Save in Photos") works on the full-size image. */}
|
long-press menu ("Save in Photos") works on the full-size image. */}
|
||||||
<img
|
<img
|
||||||
|
ref={imgRef}
|
||||||
src={current.url}
|
src={current.url}
|
||||||
alt={current.filename}
|
alt={current.filename}
|
||||||
|
onLoad={measureHintPosition}
|
||||||
className="max-h-full max-w-full object-contain [-webkit-touch-callout:default]"
|
className="max-h-full max-w-full object-contain [-webkit-touch-callout:default]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user