fix: keep hover previews near pointer

This commit is contained in:
2026-05-20 19:50:56 +08:00
parent 5a05058dd0
commit cacb0bd40c

View File

@@ -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
)} )}
</> </>
); );