@cplieger/web-terminal-engine 2.3.2 → 2.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/jsr.json +1 -1
- package/package.json +4 -4
- package/src/scroll.ts +43 -8
package/jsr.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cplieger/web-terminal-engine",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"typecheck": "tsc -p tsconfig.json",
|
|
@@ -15,11 +15,11 @@
|
|
|
15
15
|
"@typescript/native": "npm:typescript@7.0.2",
|
|
16
16
|
"@vitest/coverage-v8": "4.1.10",
|
|
17
17
|
"esbuild": "0.28.1",
|
|
18
|
-
"eslint": "10.
|
|
19
|
-
"fast-check": "4.
|
|
18
|
+
"eslint": "10.7.0",
|
|
19
|
+
"fast-check": "4.9.0",
|
|
20
20
|
"happy-dom": "20.10.6",
|
|
21
21
|
"pngjs": "7.0.0",
|
|
22
|
-
"prettier": "3.9.
|
|
22
|
+
"prettier": "3.9.5",
|
|
23
23
|
"typescript": "npm:@typescript/typescript6@6.0.2",
|
|
24
24
|
"typescript-eslint": "8.63.0",
|
|
25
25
|
"vitest": "4.1.10"
|
package/src/scroll.ts
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
//
|
|
3
3
|
// One piece of state: `following`. The user is "following" when the viewport is
|
|
4
4
|
// at (or within a small tolerance of) the bottom; otherwise they have scrolled
|
|
5
|
-
// up to read and are "holding". The state is derived purely from scroll
|
|
6
|
-
// position
|
|
7
|
-
// timer, and no programmatic-vs-user flag. That heuristic soup (a
|
|
8
|
-
// tolerance, a 150ms debounce, a 60-second touch window) was the source
|
|
9
|
-
// view-jumping and scroll-interruption bugs (the legacy heuristic-soup
|
|
5
|
+
// up to read and are "holding". The state is derived purely from the scroll
|
|
6
|
+
// events — position plus movement direction — with no debounce window, no
|
|
7
|
+
// suppress timer, and no programmatic-vs-user flag. That heuristic soup (a
|
|
8
|
+
// 100px tolerance, a 150ms debounce, a 60-second touch window) was the source
|
|
9
|
+
// of the view-jumping and scroll-interruption bugs (the legacy heuristic-soup
|
|
10
10
|
// failure mode; see the #web-terminal-engine steering doc, "Design rationale").
|
|
11
11
|
//
|
|
12
12
|
// The renderer calls stickToBottom() once after each flush: if following, pin
|
|
@@ -15,13 +15,32 @@
|
|
|
15
15
|
// Appending content at the bottom does not fire a scroll event, so following
|
|
16
16
|
// stays true across new output and the post-flush pin lands correctly. Pinning
|
|
17
17
|
// to the bottom produces a scroll event whose recomputation yields
|
|
18
|
-
// following=true again (no churn
|
|
19
|
-
//
|
|
18
|
+
// following=true again (no churn; the pin only ever moves DOWN). Scrolling
|
|
19
|
+
// back to within the tolerance of the bottom re-engages following.
|
|
20
|
+
//
|
|
21
|
+
// Disengaging is direction-based, not tolerance-based: ANY upward movement
|
|
22
|
+
// that leaves a real gap below flips to holding at once. A tolerance-only
|
|
23
|
+
// rule lost a race under heavy streaming — the renderer flushes (and pins)
|
|
24
|
+
// every frame, so each frame's upward scroll increment restarted from the
|
|
25
|
+
// bottom the previous pin reset it to, and unless a single frame's delta
|
|
26
|
+
// exceeded the tolerance the user was yanked back down every few milliseconds
|
|
27
|
+
// (the "scroll up fights me during output" bug). Per the HTML event loop the
|
|
28
|
+
// scroll steps run before that frame's rAF flush, so the first upward tick
|
|
29
|
+
// flips holding before the next pin can fire. The nothing-below guard keeps
|
|
30
|
+
// one non-user case following: when content SHRINKS (top-row eviction, a
|
|
31
|
+
// clear), the browser clamps scrollTop DOWN to the new maximum — an upward
|
|
32
|
+
// move that lands exactly at the bottom, which must not break auto-follow.
|
|
20
33
|
|
|
21
34
|
const BOTTOM_TOLERANCE_PX = 24;
|
|
35
|
+
// An upward move only disengages follow when a real gap is left below it.
|
|
36
|
+
// Bigger than 0 to absorb fractional-layout rounding in the shrink-clamp case
|
|
37
|
+
// (a clamp lands at the bottom, but scrollTop can be subpixel-off); far below
|
|
38
|
+
// any real one-frame user scroll increment.
|
|
39
|
+
const CLAMP_EPSILON_PX = 1;
|
|
22
40
|
|
|
23
41
|
let scrollEl: HTMLElement | null = null;
|
|
24
42
|
let following = true;
|
|
43
|
+
let lastScrollTop = 0;
|
|
25
44
|
let onFollowChange: ((scrolledUp: boolean) => void) | null = null;
|
|
26
45
|
let scrollHandler: (() => void) | null = null;
|
|
27
46
|
|
|
@@ -65,8 +84,24 @@ export function init(opts: {
|
|
|
65
84
|
scrollEl = opts.scrollEl;
|
|
66
85
|
onFollowChange = opts.onUserScrollChange ?? null;
|
|
67
86
|
following = true;
|
|
87
|
+
lastScrollTop = scrollEl.scrollTop;
|
|
68
88
|
scrollHandler = () => {
|
|
69
|
-
|
|
89
|
+
if (!scrollEl) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const top = scrollEl.scrollTop;
|
|
93
|
+
const movedUp = top < lastScrollTop;
|
|
94
|
+
lastScrollTop = top;
|
|
95
|
+
// Upward movement with content still below = the user pulling away from
|
|
96
|
+
// the live tail: hold immediately, however small the move (see the header
|
|
97
|
+
// comment for why the tolerance alone lost the streaming race). An upward
|
|
98
|
+
// move that lands AT the bottom is the browser clamping after a content
|
|
99
|
+
// shrink, not the user; everything else derives from position as before.
|
|
100
|
+
if (movedUp && distanceFromBottom() > CLAMP_EPSILON_PX) {
|
|
101
|
+
setFollowing(false);
|
|
102
|
+
} else {
|
|
103
|
+
setFollowing(atBottom());
|
|
104
|
+
}
|
|
70
105
|
};
|
|
71
106
|
scrollEl.addEventListener("scroll", scrollHandler, { passive: true });
|
|
72
107
|
}
|