1export type ComposerScrollGestureState = {
2 accumulatedDeltaPx: number;
3 collapseSuppressed: boolean;
4 lastEventAt: number;
5};
6
7export function shouldCollapseComposerForScrollKey(input: {
8 key: string;
9 scrollTop: number;
10 scrollHeight: number;
11 clientHeight: number;
12 isAtLogicalEnd: boolean;
13}): boolean {
14 switch (input.key) {
15 case "PageUp":
16 case "Home":
17 return input.scrollTop > 1;
18 case "PageDown":
19 case "End":
20 return !input.isAtLogicalEnd && input.scrollTop < input.scrollHeight - input.clientHeight - 1;
21 default:
22 return false;
23 }
24}
25
26export function createComposerScrollGestureState(): ComposerScrollGestureState {
27 return {
28 accumulatedDeltaPx: 0,
29 collapseSuppressed: false,
30 lastEventAt: Number.NEGATIVE_INFINITY,
31 };
32}
33
34export function resetComposerScrollGesture(state: ComposerScrollGestureState): void {
35 state.accumulatedDeltaPx = 0;
36 state.collapseSuppressed = false;
37 state.lastEventAt = Number.NEGATIVE_INFINITY;
38}
39
40export function suppressActiveComposerScrollGesture(
41 state: ComposerScrollGestureState,
42 now: number,
43 gestureResetMs: number,
44): void {
45 if (now - state.lastEventAt <= gestureResetMs) {
46 state.collapseSuppressed = true;
47 }
48}
49
50export function recordComposerScrollGestureEvent(
51 state: ComposerScrollGestureState,
52 input: {
53 now: number;
54 deltaPx: number;
55 collapseThresholdPx: number;
56 collapseEligible: boolean;
57 canScrollInGestureDirection: boolean;
58 scrollsTowardLogicalEnd: boolean;
59 },
60): boolean {
61 state.lastEventAt = input.now;
62 if (
63 state.collapseSuppressed ||
64 !input.collapseEligible ||
65 !input.canScrollInGestureDirection ||
66 input.scrollsTowardLogicalEnd
67 ) {
68 state.accumulatedDeltaPx = 0;
69 return false;
70 }
71
72 state.accumulatedDeltaPx += input.deltaPx;
73 if (state.accumulatedDeltaPx < input.collapseThresholdPx) {
74 return false;
75 }
76
77 state.accumulatedDeltaPx = 0;
78 return true;
79}