feat(ui): keyboard navigation and focus restore in album grid

- Cursor separate from selection; arrow keys move it, Space/Shift+Arrow/Enter/Esc act
- Click on empty grid space clears selection back to album properties
- Focus returns to the last viewed photo after closing the viewer
This commit is contained in:
iu
2026-10-11 14:51:00 +03:00
parent d99ac77587
commit 7359495a73
9 changed files with 588 additions and 5 deletions
+22 -2
View File
@@ -6,16 +6,31 @@
interface Props {
photos: Photo[];
selectedIds?: string[];
cursorIndex?: number | null;
cursorVisible?: boolean;
onSelect?: (index: number, event: MouseEvent) => void;
onOpen?: (index: number) => void;
onDragStart?: (index: number, event: DragEvent) => void;
onContextMenu?: (index: number, event: MouseEvent) => void;
onCursorFocus?: (index: number) => void;
}
let { photos, selectedIds = [], onSelect, onOpen, onDragStart, onContextMenu }: Props =
$props();
let {
photos,
selectedIds = [],
cursorIndex = null,
cursorVisible = false,
onSelect,
onOpen,
onDragStart,
onContextMenu,
onCursorFocus
}: Props = $props();
const isSelected = (id: string) => selectedIds.includes(id);
const isCursor = (i: number) => cursorIndex === i;
const tabIndexFor = (i: number) =>
cursorIndex == null ? (i === 0 ? 0 : -1) : isCursor(i) ? 0 : -1;
</script>
{#if photos.length === 0}
@@ -29,13 +44,18 @@
<li>
<button
type="button"
data-photo-cell
tabindex={tabIndexFor(i)}
draggable={isSelected(photo.id)}
class="border-border bg-bg-muted group relative block aspect-square w-full overflow-hidden rounded-lg border {isSelected(
photo.id
)
? 'ring-primary ring-2'
: ''} {isCursor(i) && cursorVisible
? 'outline outline-2 outline-offset-2 outline-text'
: ''}"
aria-pressed={isSelected(photo.id)}
onfocus={() => onCursorFocus?.(i)}
onclick={(e) => onSelect?.(i, e)}
ondblclick={() => onOpen?.(i)}
ondragstart={(e) => onDragStart?.(i, e)}