mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
fix(ui): overlay scrollbars, resizer hit-test, and Linux mini wheel (#255)
Add OverlayScrollArea with shared thumb metrics and drag handling; size the thumb against the rail track height so panel insets cannot push it past the visible rail. Route scroll uses a stable viewport id; Genres restores scroll and infinite-scroll observation against that viewport (merged with upstream virtualized genres list and lazy routes behind Suspense). Suppress queue resizer activation when the pointer targets the main-route overlay scrollbar; disable the resizer while dragging the thumb and use a grabbing cursor on the body. Apply WebKitGTK smooth wheel to the mini webview as well as main; the mini window reapplies the persisted setting after auth store hydration.
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
export type OverlayScrollbarThumbMeta = {
|
||||
thumbH: number;
|
||||
thumbT: number;
|
||||
visible: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param trackHeight — pixel height of the overlay rail (inset top/bottom).
|
||||
* When shorter than the viewport, thumb size/position must use this or the
|
||||
* thumb’s bottom extends past the visible rail at max scroll.
|
||||
*/
|
||||
export function computeOverlayScrollbarThumbMeta(
|
||||
el: HTMLElement | null,
|
||||
trackHeight?: number,
|
||||
): OverlayScrollbarThumbMeta {
|
||||
if (!el) return { thumbH: 0, thumbT: 0, visible: false };
|
||||
const { scrollTop, scrollHeight, clientHeight } = el;
|
||||
if (scrollHeight <= clientHeight + 1) {
|
||||
return { thumbH: 0, thumbT: 0, visible: false };
|
||||
}
|
||||
const th =
|
||||
trackHeight != null && trackHeight > 0 ? Math.min(trackHeight, clientHeight) : clientHeight;
|
||||
const ratio = clientHeight / scrollHeight;
|
||||
const rawH = Math.round(ratio * th);
|
||||
const thumbH = Math.min(th, Math.max(24, rawH));
|
||||
const range = Math.max(0, th - thumbH);
|
||||
const scrollRange = scrollHeight - clientHeight;
|
||||
const thumbT =
|
||||
scrollRange > 0
|
||||
? Math.min(range, Math.max(0, Math.round((scrollTop / scrollRange) * range)))
|
||||
: 0;
|
||||
return { thumbH, thumbT, visible: true };
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import type { PointerEvent as ReactPointerEvent } from 'react';
|
||||
import { computeOverlayScrollbarThumbMeta } from './overlayScrollbarMetrics';
|
||||
|
||||
/**
|
||||
* Drag the overlay scrollbar thumb (native bar is hidden). Maps pointer delta
|
||||
* to scrollTop using the same thumb/range geometry as the visual thumb.
|
||||
*/
|
||||
export function bindOverlayScrollbarThumbDrag(
|
||||
e: ReactPointerEvent<HTMLElement>,
|
||||
scrollEl: HTMLDivElement | null,
|
||||
): void {
|
||||
if (e.button !== 0 || !scrollEl) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const thumb = e.currentTarget;
|
||||
const rail = thumb.parentElement;
|
||||
const trackH =
|
||||
rail instanceof HTMLElement && rail.clientHeight > 0 ? rail.clientHeight : scrollEl.clientHeight;
|
||||
const meta = computeOverlayScrollbarThumbMeta(scrollEl, trackH);
|
||||
if (!meta.visible) return;
|
||||
|
||||
const { scrollHeight, clientHeight } = scrollEl;
|
||||
const scrollRange = scrollHeight - clientHeight;
|
||||
const range = trackH - meta.thumbH;
|
||||
if (range <= 1) return;
|
||||
|
||||
const startScroll = scrollEl.scrollTop;
|
||||
const startY = e.clientY;
|
||||
const pointerId = e.pointerId;
|
||||
|
||||
thumb.classList.add('is-thumb-dragging');
|
||||
document.body.classList.add('is-overlay-scrollbar-thumb-drag');
|
||||
try {
|
||||
thumb.setPointerCapture(pointerId);
|
||||
} catch {
|
||||
thumb.classList.remove('is-thumb-dragging');
|
||||
document.body.classList.remove('is-overlay-scrollbar-thumb-drag');
|
||||
return;
|
||||
}
|
||||
|
||||
const onMove = (ev: PointerEvent) => {
|
||||
if (ev.pointerId !== pointerId) return;
|
||||
const dy = ev.clientY - startY;
|
||||
const next = startScroll + (dy / range) * scrollRange;
|
||||
scrollEl.scrollTop = Math.max(0, Math.min(scrollRange, next));
|
||||
};
|
||||
|
||||
const onUp = (ev: PointerEvent) => {
|
||||
if (ev.pointerId !== pointerId) return;
|
||||
thumb.classList.remove('is-thumb-dragging');
|
||||
document.body.classList.remove('is-overlay-scrollbar-thumb-drag');
|
||||
try {
|
||||
thumb.releasePointerCapture(pointerId);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
window.removeEventListener('pointermove', onMove);
|
||||
window.removeEventListener('pointerup', onUp);
|
||||
window.removeEventListener('pointercancel', onUp);
|
||||
};
|
||||
|
||||
window.addEventListener('pointermove', onMove);
|
||||
window.addEventListener('pointerup', onUp);
|
||||
window.addEventListener('pointercancel', onUp);
|
||||
}
|
||||
Reference in New Issue
Block a user