fix(media viewer): lightbox focuses itself on open (keys worked only after a click); timeline viewer gets +/-/0 keys, double-click zoom and dialog semantics (#164)
CI / Build & Quality Checks (push) Successful in 1m31s
CI / Docker image build & smoke test (push) Skipped
CI / Secret scan (gitleaks) (push) Successful in 5s
CI / Trigger Desktop Build (push) Successful in 7s
CI / Playwright smoke (e2e) (push) Successful in 2m19s
CI / Build & Quality Checks (push) Successful in 1m31s
CI / Docker image build & smoke test (push) Skipped
CI / Secret scan (gitleaks) (push) Successful in 5s
CI / Trigger Desktop Build (push) Successful in 7s
CI / Playwright smoke (e2e) (push) Successful in 2m19s
Gallery lightbox: FocusTrap had initialFocus:false, so focus stayed on the tile behind the overlay and ←/→/Esc/+/- were dead until the user clicked inside. Timeline ImageViewer: no keyboard zoom, no role/aria-modal/label, focus landed on the <img>; now mirrors the lightbox (+ = / - / 0, double-click toggles 1×↔2×, role=dialog aria-modal labelled by the file name, focuses on open). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import classNames from 'classnames';
|
||||
import { Box, Chip, Header, Icon, IconButton, Icons, Text, as } from 'folds';
|
||||
@@ -27,12 +27,37 @@ export const ImageViewer = as<'div', ImageViewerProps>(
|
||||
saveFile(fileContent, alt);
|
||||
};
|
||||
|
||||
// [Gitea #164] Same keyboard model as the gallery lightbox: + / = zoom
|
||||
// in, - zoom out, 0 reset; double-click toggles 1× ↔ 2×. The root takes
|
||||
// focus on open so those keys (and Escape, handled by the modal) work
|
||||
// immediately instead of only after clicking inside the viewer.
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
useEffect(() => {
|
||||
rootRef.current?.focus({ preventScroll: true });
|
||||
}, []);
|
||||
const handleKeyDown = (evt: React.KeyboardEvent) => {
|
||||
if (evt.key === '+' || evt.key === '=') zoomIn();
|
||||
else if (evt.key === '-') zoomOut();
|
||||
else if (evt.key === '0') setZoom(1);
|
||||
else return;
|
||||
evt.preventDefault();
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
className={classNames(css.ImageViewer, className)}
|
||||
direction="Column"
|
||||
role="dialog"
|
||||
aria-modal
|
||||
aria-label={alt || 'Image viewer'}
|
||||
tabIndex={-1}
|
||||
onKeyDown={handleKeyDown}
|
||||
{...props}
|
||||
ref={ref}
|
||||
ref={(node: HTMLDivElement | null) => {
|
||||
rootRef.current = node;
|
||||
if (typeof ref === 'function') ref(node);
|
||||
else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node;
|
||||
}}
|
||||
>
|
||||
<Header className={css.ImageViewerHeader} size="400">
|
||||
<Box grow="Yes" alignItems="Center" gap="200">
|
||||
@@ -116,6 +141,7 @@ export const ImageViewer = as<'div', ImageViewerProps>(
|
||||
alt={alt}
|
||||
onMouseDown={onMouseDown}
|
||||
onTouchStart={onTouchStart}
|
||||
onDoubleClick={() => setZoom(zoom === 1 ? 2 : 1)}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -302,6 +302,7 @@ function Lightbox({
|
||||
// Pan is only active for a zoomed-in image; usePan resets its offset when this
|
||||
// flips false (i.e. back to 1x, on navigation, or on a video).
|
||||
const { pan, cursor, onMouseDown, onTouchStart } = usePan(isImage && zoom !== 1);
|
||||
const dialogRef = useRef<HTMLDivElement>(null);
|
||||
const toggleZoom = useCallback(() => setZoom((z) => (z === 1 ? 2 : 1)), [setZoom]);
|
||||
|
||||
// Reset zoom when navigating to another item (and thus pan, via usePan).
|
||||
@@ -346,12 +347,17 @@ function Lightbox({
|
||||
<Overlay open backdrop={<OverlayBackdrop />}>
|
||||
<FocusTrap
|
||||
focusTrapOptions={{
|
||||
initialFocus: false,
|
||||
// Focus the dialog itself on open: with `initialFocus: false` focus
|
||||
// stayed on the gallery tile behind the overlay, so ←/→/Esc/+/-
|
||||
// (handled by onKeyDown below) did nothing until the user clicked
|
||||
// inside the viewer (Gitea #164).
|
||||
initialFocus: () => dialogRef.current ?? false,
|
||||
clickOutsideDeactivates: false,
|
||||
escapeDeactivates: false,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={dialogRef}
|
||||
role="dialog"
|
||||
aria-modal
|
||||
aria-label="Media viewer"
|
||||
|
||||
Reference in New Issue
Block a user