Files
psysonic/src/hooks/useFolderBrowserScrolling.ts
T
Frank Stellmacher d4d3b0e53f refactor(folder-browser): G.87 — extract column component + 3 hooks (cluster, multi-commit) (#654)
* refactor(folder-browser): G.87.1 — extract helpers + types

Move ColumnKind / NavPos / Column types + entryToAlbumIfPresent /
entryToTrack mappers + isFolderBrowserArrowKey /
folderBrowserHasKeyModifiers key-event helpers into
src/utils/folderBrowserHelpers.ts. Pure code move.

* refactor(folder-browser): G.87.2 — extract FolderBrowserColumn component

* refactor(folder-browser): G.87.3 — extract useFolderBrowserNowPlayingPath hook

* refactor(folder-browser): G.87.4 — extract useFolderBrowserScrolling hook

* refactor(folder-browser): G.87.5 — extract useFolderBrowserKeyboardNav hook
2026-05-13 18:06:47 +02:00

92 lines
3.1 KiB
TypeScript

import { useEffect, useRef, useState } from 'react';
import type { Column, NavPos } from '../utils/folderBrowserHelpers';
interface Args {
columns: Column[];
keyboardPos: NavPos | null;
keyboardNavActive: boolean;
setKeyboardNavActive: React.Dispatch<React.SetStateAction<boolean>>;
}
interface Result {
wrapperRef: React.RefObject<HTMLDivElement | null>;
columnsViewportWidth: number;
}
export function useFolderBrowserScrolling({
columns, keyboardPos, keyboardNavActive, setKeyboardNavActive,
}: Args): Result {
const wrapperRef = useRef<HTMLDivElement>(null);
const [columnsViewportWidth, setColumnsViewportWidth] = useState(0);
useEffect(() => {
const el = wrapperRef.current;
if (!el) return;
requestAnimationFrame(() => {
el.scrollLeft = el.scrollWidth;
});
}, [columns.length]);
useEffect(() => {
const el = wrapperRef.current;
if (!el) return;
setColumnsViewportWidth(el.clientWidth);
const observer = new ResizeObserver(() => {
setColumnsViewportWidth(el.clientWidth);
});
observer.observe(el);
return () => observer.disconnect();
}, []);
useEffect(() => {
if (!wrapperRef.current) return;
requestAnimationFrame(() => {
columns.forEach((col, colIndex) => {
const selectedId = col.selectedId;
if (!selectedId) return;
const row = wrapperRef.current?.querySelector<HTMLElement>(
`.folder-col[data-folder-col-index="${colIndex}"] .folder-col-row[data-item-id="${selectedId}"]`,
);
row?.scrollIntoView({ block: 'nearest' });
});
if (keyboardPos) {
const kbdRow = wrapperRef.current?.querySelector<HTMLElement>(
`.folder-col[data-folder-col-index="${keyboardPos.colIndex}"] .folder-col-row[data-row-index="${keyboardPos.rowIndex}"]`,
);
kbdRow?.scrollIntoView({ block: 'nearest' });
}
const fallbackColIndex = [...columns]
.map((c, i) => (c.selectedId ? i : -1))
.filter(i => i >= 0)
.pop();
const baseColIndex = keyboardPos?.colIndex ?? fallbackColIndex ?? Math.max(0, columns.length - 1);
const focusColIndex = Math.min(Math.max(0, columns.length - 1), baseColIndex + 1);
const focusCol = wrapperRef.current?.querySelector<HTMLElement>(
`.folder-col[data-folder-col-index="${focusColIndex}"]`,
);
focusCol?.scrollIntoView({ block: 'nearest', inline: 'nearest' });
});
}, [columns, keyboardPos]);
useEffect(() => {
const el = wrapperRef.current;
if (!el) return;
const hasRows = columns.some(c => !c.loading && !c.error && c.items.length > 0);
if (!hasRows) return;
requestAnimationFrame(() => {
el.focus({ preventScroll: true });
});
}, [columns]);
useEffect(() => {
if (!keyboardNavActive) return;
const onMouseMove = () => setKeyboardNavActive(false);
window.addEventListener('mousemove', onMouseMove, { once: true });
return () => window.removeEventListener('mousemove', onMouseMove);
}, [keyboardNavActive, setKeyboardNavActive]);
return { wrapperRef, columnsViewportWidth };
}