fix: keep hover previews near pointer
This commit is contained in:
@@ -1,12 +1,14 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
import type { PointerEvent } from 'react';
|
import type { PointerEvent } from 'react';
|
||||||
|
|
||||||
type PreviewState = {
|
type PreviewState = {
|
||||||
left: number;
|
left: number;
|
||||||
top: number;
|
top: number;
|
||||||
width: number;
|
width: number;
|
||||||
|
height: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
function parseRatio(aspectRatio?: string) {
|
function parseRatio(aspectRatio?: string) {
|
||||||
@@ -18,17 +20,33 @@ function parseRatio(aspectRatio?: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function nextPreviewState(event: PointerEvent<HTMLElement>, aspectRatio?: string): PreviewState {
|
function nextPreviewState(event: PointerEvent<HTMLElement>, aspectRatio?: string): PreviewState {
|
||||||
const gap = 18;
|
const gap = 12;
|
||||||
const margin = 12;
|
const margin = 10;
|
||||||
const ratio = parseRatio(aspectRatio);
|
const ratio = parseRatio(aspectRatio);
|
||||||
const maxWidth = ratio < 0.8 ? 380 : ratio > 1.35 ? 620 : 500;
|
const maxWidth = ratio < 0.8 ? 340 : ratio > 1.35 ? 520 : 420;
|
||||||
const width = Math.min(maxWidth, Math.max(260, window.innerWidth * 0.38));
|
const maxHeight = Math.min(window.innerHeight * 0.68, window.innerHeight - margin * 2);
|
||||||
const height = Math.min(window.innerHeight * 0.82, width / ratio);
|
let width = Math.min(maxWidth, Math.max(240, window.innerWidth * 0.28));
|
||||||
let left = event.clientX + gap;
|
let height = width / ratio;
|
||||||
let top = event.clientY + gap;
|
|
||||||
|
if (height > maxHeight) {
|
||||||
|
height = maxHeight;
|
||||||
|
width = height * ratio;
|
||||||
|
}
|
||||||
|
|
||||||
|
const canOpenRight = event.clientX + gap + width <= window.innerWidth - margin;
|
||||||
|
const canOpenLeft = event.clientX - gap - width >= margin;
|
||||||
|
let left = canOpenRight || !canOpenLeft
|
||||||
|
? event.clientX + gap
|
||||||
|
: event.clientX - width - gap;
|
||||||
|
|
||||||
|
const canOpenBelow = event.clientY + gap + height <= window.innerHeight - margin;
|
||||||
|
const canOpenAbove = event.clientY - gap - height >= margin;
|
||||||
|
let top = canOpenBelow || !canOpenAbove
|
||||||
|
? event.clientY + gap
|
||||||
|
: event.clientY - height - gap;
|
||||||
|
|
||||||
if (left + width > window.innerWidth - margin) {
|
if (left + width > window.innerWidth - margin) {
|
||||||
left = event.clientX - width - gap;
|
left = window.innerWidth - width - margin;
|
||||||
}
|
}
|
||||||
if (top + height > window.innerHeight - margin) {
|
if (top + height > window.innerHeight - margin) {
|
||||||
top = window.innerHeight - height - margin;
|
top = window.innerHeight - height - margin;
|
||||||
@@ -37,6 +55,7 @@ function nextPreviewState(event: PointerEvent<HTMLElement>, aspectRatio?: string
|
|||||||
left: Math.max(margin, left),
|
left: Math.max(margin, left),
|
||||||
top: Math.max(margin, top),
|
top: Math.max(margin, top),
|
||||||
width,
|
width,
|
||||||
|
height,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,6 +73,11 @@ export function HoverImagePreview({
|
|||||||
onImageLoad?: (image: HTMLImageElement) => void;
|
onImageLoad?: (image: HTMLImageElement) => void;
|
||||||
}) {
|
}) {
|
||||||
const [preview, setPreview] = useState<PreviewState | null>(null);
|
const [preview, setPreview] = useState<PreviewState | null>(null);
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -68,13 +92,18 @@ export function HoverImagePreview({
|
|||||||
onPointerLeave={() => setPreview(null)}
|
onPointerLeave={() => setPreview(null)}
|
||||||
onLoad={event => onImageLoad?.(event.currentTarget)}
|
onLoad={event => onImageLoad?.(event.currentTarget)}
|
||||||
/>
|
/>
|
||||||
{preview && (
|
{preview && mounted && createPortal(
|
||||||
<div
|
<div
|
||||||
className="pointer-events-none fixed z-[90] rounded-[8px] bg-white p-2 shadow-[0_24px_80px_-24px_rgba(0,0,0,0.86)] ring-1 ring-white/20"
|
className="pointer-events-none fixed z-[90] overflow-hidden rounded-[8px] bg-white shadow-[0_24px_80px_-24px_rgba(0,0,0,0.86)] ring-1 ring-white/20"
|
||||||
style={{ left: preview.left, top: preview.top, width: preview.width }}
|
style={{ left: preview.left, top: preview.top, width: preview.width, height: preview.height }}
|
||||||
>
|
>
|
||||||
<img src={src} alt="" className="max-h-[82vh] w-full object-contain" style={{ aspectRatio: parseRatio(aspectRatio) }} />
|
<img
|
||||||
</div>
|
src={src}
|
||||||
|
alt=""
|
||||||
|
className="h-full w-full object-contain"
|
||||||
|
/>
|
||||||
|
</div>,
|
||||||
|
document.body
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user