(frontend) close file preview on backdrop click

Users can now dismiss the preview by clicking the blurry area
around the content. Each viewer marks its backdrop zones with
a data attribute so the handler in FilesPreview can distinguish
backdrop from content clicks. The image viewer also guards
against accidental close during pan-drag gestures.
This commit is contained in:
Nathan Vasse
2026-04-17 16:48:54 +02:00
parent b69e0fc218
commit afb4d2862d
8 changed files with 44 additions and 8 deletions

View File

@@ -129,6 +129,16 @@ export const FilePreview = ({
window.open(currentFile.url_preview, "_blank");
};
const handleBackdropClick = (e: React.MouseEvent<HTMLDivElement>) => {
if (!(e.target instanceof HTMLElement)) return;
if (
e.target === e.currentTarget ||
e.target.dataset.previewBackdrop === "true"
) {
onClose?.();
}
};
// Render the appropriate viewer based on file category
const renderViewer = () => {
if (!currentFile) {
@@ -165,7 +175,10 @@ export const FilePreview = ({
);
case MimeCategory.VIDEO:
return (
<div className="video-preview-viewer-container">
<div
className="video-preview-viewer-container"
data-preview-backdrop="true"
>
<div className="video-preview-viewer">
<VideoPlayer
src={currentFile.url_preview}
@@ -177,7 +190,10 @@ export const FilePreview = ({
);
case MimeCategory.AUDIO:
return (
<div className="video-preview-viewer-container">
<div
className="video-preview-viewer-container"
data-preview-backdrop="true"
>
<div className="video-preview-viewer">
<AudioPlayer
src={currentFile.url_preview}
@@ -302,6 +318,7 @@ export const FilePreview = ({
>
<div data-testid="file-preview">
<div
onClick={handleBackdropClick}
className={clsx(
"file-preview__container",
isSidebarOpen && "file-preview__container--sidebar-open",

View File

@@ -55,6 +55,7 @@ export const ImageViewer: React.FC<ImageViewerProps> = ({
const containerRef = useRef<HTMLDivElement>(null);
const imageRef = useRef<HTMLImageElement>(null);
const mouseDownPosRef = useRef<{ x: number; y: number } | null>(null);
// Calculate distance between two touch points
const getTouchDistance = useCallback(
@@ -193,6 +194,7 @@ export const ImageViewer: React.FC<ImageViewerProps> = ({
// Handle mouse down for dragging
const handleMouseDown = useCallback(
(e: React.MouseEvent) => {
mouseDownPosRef.current = { x: e.clientX, y: e.clientY };
if (isImageExceedingBounds()) {
setIsDragging(true);
setDragStart({
@@ -204,6 +206,20 @@ export const ImageViewer: React.FC<ImageViewerProps> = ({
[isImageExceedingBounds, position],
);
// Swallow the click that follows a pan-drag so it never bubbles to the
// FilePreview backdrop handler. Without this, releasing a drag on the
// empty area of the container would close the preview.
const handleClickCapture = useCallback((e: React.MouseEvent) => {
const start = mouseDownPosRef.current;
mouseDownPosRef.current = null;
if (!start) return;
const dx = e.clientX - start.x;
const dy = e.clientY - start.y;
if (dx * dx + dy * dy > 16) {
e.stopPropagation();
}
}, []);
// Handle mouse move for dragging
const handleMouseMove = useCallback(
(e: React.MouseEvent) => {
@@ -359,11 +375,13 @@ export const ImageViewer: React.FC<ImageViewerProps> = ({
<div
ref={containerRef}
className="image-viewer__container"
data-preview-backdrop="true"
tabIndex={0}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
onClickCapture={handleClickCapture}
onWheel={handleWheel}
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}

View File

@@ -24,7 +24,7 @@ export const NotSupportedPreview = ({
}, [onDownload, file]);
return (
<div className="file-preview-unsupported">
<div className="file-preview-unsupported" data-preview-backdrop="true">
<div className="file-preview-unsupported__icon">
<FileIcon file={file} size="xlarge" />
</div>

View File

@@ -5,7 +5,7 @@ export const OutdatedBrowserPreview = () => {
const { t } = useTranslation();
return (
<div className="file-preview-unsupported">
<div className="file-preview-unsupported" data-preview-backdrop="true">
<div className="file-preview-unsupported__icon">
<Icon name="security" type={IconType.OUTLINED} size={48} />
</div>

View File

@@ -137,6 +137,7 @@ export function PdfPageViewer({
const rowRenderer: ListRowRenderer = ({ index, key, style }) => (
<div
key={key}
data-preview-backdrop="true"
style={{
...style,
display: "flex",

View File

@@ -114,7 +114,7 @@ export function PdfPreview({
if (error?.message || documentError === "generic") {
return (
<div className="file-preview-unsupported">
<div className="file-preview-unsupported" data-preview-backdrop="true">
<div className="file-preview-unsupported__icon">
<Icon name="error" type={IconType.OUTLINED} size={48} />
</div>
@@ -140,7 +140,7 @@ export function PdfPreview({
return (
<div className="pdf-preview">
<div className="pdf-preview__body">
<div className="pdf-preview__body" data-preview-backdrop="true">
{file ? (
<Document
file={file}

View File

@@ -14,7 +14,7 @@ export const SuspiciousPreview = ({
const { t } = useTranslation();
return (
<div className="file-preview-suspicious">
<div className="file-preview-suspicious" data-preview-backdrop="true">
<div className="file-preview-suspicious__icon">
<img src={mimeSuspicious.src} alt="" className="item-icon xlarge" />
</div>

View File

@@ -13,7 +13,7 @@ export const WopiOpenInEditor = ({ file }: WopiOpenInEditorProps) => {
const { t } = useTranslation();
return (
<div className="file-preview-unsupported">
<div className="file-preview-unsupported" data-preview-backdrop="true">
<div className="file-preview-unsupported__icon">
<FileIcon file={file} size="xlarge" />
</div>