fix inner scroll behavior
This commit is contained in:
72
src/utils/nested-scroll.ts
Normal file
72
src/utils/nested-scroll.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Nested scroll sections favor the innermost one under the cursor.
|
||||
*
|
||||
* opentui bubbles a wheel event up the renderable tree, so every ancestor
|
||||
* `ScrollBoxRenderable` that has room to move scrolls — nested sections (e.g.
|
||||
* the episode-description scrollbox inside a page's list pane) scroll in
|
||||
* lockstep. This patches the scrollbox's wheel handler so the innermost
|
||||
* scrollbox under the cursor wins instead:
|
||||
*
|
||||
* • The first scrollbox that can move in the wheel's direction scrolls and
|
||||
* stops propagation, so its ancestors don't also scroll.
|
||||
* • When it is already at its boundary it lets the next outer scrollbox
|
||||
* take over (wheel chaining), matching typical nested-scroll UX.
|
||||
*/
|
||||
|
||||
import { ScrollBoxRenderable } from "@opentui/core";
|
||||
import type { MouseEvent } from "@opentui/core";
|
||||
|
||||
type ScrollDir = "up" | "down" | "left" | "right";
|
||||
|
||||
// The scrollbox's own wheel handler (scrolls, then bubbles to its parent).
|
||||
const original = ScrollBoxRenderable.prototype.onMouseEvent;
|
||||
|
||||
let installed = false;
|
||||
|
||||
/** True when `sb` has room to move in `dir` from its current position. */
|
||||
function canScroll(sb: ScrollBoxRenderable, dir: ScrollDir): boolean {
|
||||
const maxTop = Math.max(0, sb.scrollHeight - sb.viewport.height);
|
||||
const maxLeft = Math.max(0, sb.scrollWidth - sb.viewport.width);
|
||||
switch (dir) {
|
||||
case "up":
|
||||
return sb.scrollTop > 0;
|
||||
case "down":
|
||||
return sb.scrollTop < maxTop;
|
||||
case "left":
|
||||
return sb.scrollLeft > 0;
|
||||
case "right":
|
||||
return sb.scrollLeft < maxLeft;
|
||||
}
|
||||
}
|
||||
|
||||
const handleWheel = function (
|
||||
this: ScrollBoxRenderable,
|
||||
event: MouseEvent,
|
||||
): void {
|
||||
if (event.type !== "scroll" || !event.scroll?.direction) {
|
||||
original.call(this, event);
|
||||
return;
|
||||
}
|
||||
|
||||
const dir = event.scroll.direction;
|
||||
const effective: ScrollDir = event.modifiers.shift
|
||||
? (dir === "up" ? "left" : dir === "down" ? "right" : dir === "right" ? "down" : "up")
|
||||
: dir;
|
||||
|
||||
const moves = canScroll(this, effective);
|
||||
original.call(this, event);
|
||||
// Only claim the wheel when this box actually moved; otherwise let the
|
||||
// next outer scrollbox (also under the cursor) take over.
|
||||
if (moves) event.stopPropagation();
|
||||
};
|
||||
|
||||
export function installNestedScrollBehavior(): void {
|
||||
if (installed || typeof original !== "function") return;
|
||||
installed = true;
|
||||
// `onMouseEvent` is a well-known protected method; the cast only bypasses
|
||||
// TypeScript's protected-access check and trusts the shipped class shape.
|
||||
const scrollboxProto = ScrollBoxRenderable.prototype as unknown as {
|
||||
onMouseEvent: typeof handleWheel;
|
||||
};
|
||||
scrollboxProto.onMouseEvent = handleWheel;
|
||||
}
|
||||
Reference in New Issue
Block a user