UI: Enhance image gallery with zoom, pan, accessibility, and focus management

This commit is contained in:
Simon Cordt
2025-09-28 07:10:30 +02:00
parent 63acb6e74b
commit 081f455c5e
+124
View File
@@ -27,6 +27,114 @@
if (e.target === overlay) UI.closeGallery();
});
// Accessibility and focus management
try {
overlay.setAttribute("role", "dialog");
overlay.setAttribute("aria-modal", "true");
overlay.tabIndex = -1;
} catch (err) { /* noop */ }
// Zoom/Pan state and helpers
UI.galleryZoom = { scale: 1, x: 0, y: 0, dragging: false, lastX: 0, lastY: 0 };
UI.updateGalleryTransform = function updateGalleryTransform() {
try {
const z = UI.galleryZoom || { scale: 1, x: 0, y: 0, dragging: false };
img.style.transform = `translate(${z.x}px, ${z.y}px) scale(${z.scale})`;
img.style.cursor = z.scale > 1 ? (z.dragging ? "grabbing" : "grab") : "default";
img.style.userSelect = "none";
img.style.willChange = "transform";
img.draggable = false;
} catch (err) { /* noop */ }
};
UI.resetGalleryTransform = function resetGalleryTransform() {
UI.galleryZoom = { scale: 1, x: 0, y: 0, dragging: false, lastX: 0, lastY: 0 };
UI.updateGalleryTransform();
};
// Double-click to toggle zoom (2x default)
img.addEventListener("dblclick", (e) => {
try {
e.preventDefault();
const z = UI.galleryZoom || (UI.galleryZoom = { scale: 1, x: 0, y: 0, dragging: false, lastX: 0, lastY: 0 });
if (z.scale === 1) {
z.scale = 2;
} else {
z.scale = 1; z.x = 0; z.y = 0;
}
UI.updateGalleryTransform();
} catch (err) { /* noop */ }
});
// Wheel for fine zoom (1x - 5x)
const onWheel = (e) => {
try {
e.preventDefault();
const z = UI.galleryZoom || (UI.galleryZoom = { scale: 1, x: 0, y: 0, dragging: false, lastX: 0, lastY: 0 });
const step = e.deltaY > 0 ? -0.2 : 0.2;
const next = Math.max(1, Math.min(5, Number(z.scale || 1) + step));
z.scale = Number(next.toFixed(2));
if (z.scale === 1) { z.x = 0; z.y = 0; }
UI.updateGalleryTransform();
} catch (err) { /* noop */ }
};
img.addEventListener("wheel", onWheel, { passive: false });
// Drag to pan when zoomed
img.addEventListener("mousedown", (e) => {
try {
if (!UI.galleryZoom || UI.galleryZoom.scale <= 1) return;
e.preventDefault();
UI.galleryZoom.dragging = true;
UI.galleryZoom.lastX = e.clientX;
UI.galleryZoom.lastY = e.clientY;
UI.updateGalleryTransform();
} catch (err) { /* noop */ }
});
const onMove = (e) => {
try {
const z = UI.galleryZoom;
if (!z || !z.dragging) return;
const dx = e.clientX - z.lastX;
const dy = e.clientY - z.lastY;
z.x += dx; z.y += dy; z.lastX = e.clientX; z.lastY = e.clientY;
UI.updateGalleryTransform();
} catch (err) { /* noop */ }
};
const onUp = () => {
try {
if (!UI.galleryZoom) return;
UI.galleryZoom.dragging = false;
UI.updateGalleryTransform();
} catch (err) { /* noop */ }
};
window.addEventListener("mousemove", onMove);
window.addEventListener("mouseup", onUp);
// Focus trap within gallery controls
UI.bindGalleryFocusTrap = function bindGalleryFocusTrap(enabled) {
const trapHandler = (e) => {
try {
if (!UI.galleryOverlay || UI.galleryOverlay.style.display !== "flex") return;
if (e.key !== "Tab") return;
const focusables = [closeBtn, prev, next].filter(Boolean);
if (!focusables.length) return;
const current = document.activeElement;
const idx = Math.max(0, focusables.indexOf(current));
e.preventDefault();
const dir = e.shiftKey ? -1 : 1;
const nextIdx = (idx + dir + focusables.length) % focusables.length;
focusables[nextIdx].focus();
} catch (err) { /* noop */ }
};
if (enabled) {
UI.galleryFocusTrapHandler = trapHandler;
overlay.addEventListener("keydown", trapHandler);
} else if (UI.galleryFocusTrapHandler) {
overlay.removeEventListener("keydown", UI.galleryFocusTrapHandler);
UI.galleryFocusTrapHandler = null;
}
};
document.body.appendChild(overlay);
UI.galleryOverlay = overlay;
UI.galleryImg = img;
@@ -42,11 +150,26 @@
UI.galleryOverlay.style.display = "flex";
UI.galleryShow(UI.galleryState.index);
UI.bindGalleryKeys(true);
UI.bindGalleryFocusTrap(true);
try {
UI._galleryPrevFocus = document.activeElement || null;
// Focus the close button initially
const btn = UI.galleryOverlay.querySelector(".pqueue-button");
if (btn && typeof btn.focus === "function") btn.focus();
else UI.galleryOverlay.focus();
} catch (err) { /* noop */ }
};
UI.closeGallery = function closeGallery() {
if (UI.galleryOverlay) UI.galleryOverlay.style.display = "none";
UI.bindGalleryKeys(false);
UI.bindGalleryFocusTrap(false);
try { UI.resetGalleryTransform && UI.resetGalleryTransform(); } catch (err) { /* noop */ }
try {
if (UI._galleryPrevFocus && document.body.contains(UI._galleryPrevFocus)) {
UI._galleryPrevFocus.focus();
}
} catch (err) { /* noop */ }
};
UI.galleryShow = function galleryShow(index) {
@@ -56,6 +179,7 @@
const desc = images[UI.galleryState.index];
UI.galleryImg.src = UI.buildPreviewUrl(desc, UI.galleryState.pid).href;
UI.galleryCounter.textContent = `${UI.galleryState.index + 1} / ${images.length}`;
try { UI.resetGalleryTransform && UI.resetGalleryTransform(); } catch (err) { /* noop */ }
};
UI.bindGalleryKeys = function bindGalleryKeys(enabled) {