Skip to content

Commit b8df921

Browse files
committed
fix: adopt hook for new inverted Android behavior
1 parent 1a2d326 commit b8df921

7 files changed

Lines changed: 310 additions & 306 deletions

File tree

src/components/KeyboardChatScrollView/useChatKeyboard/__fixtures__/testUtils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useAnimatedRef } from "react-native-reanimated";
44
import type { useChatKeyboard } from "..";
55
import type Reanimated from "react-native-reanimated";
66

7-
export type KeyboardEvent = { height: number };
7+
export type KeyboardEvent = { height: number; duration?: number };
88
export type Handlers = {
99
onStart: (e: KeyboardEvent) => void;
1010
onMove: (e: KeyboardEvent) => void;
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import { Platform } from "react-native";
2+
3+
import {
4+
type Handlers,
5+
KEYBOARD,
6+
mockLayout,
7+
mockOffset,
8+
mockScrollTo,
9+
mockSize,
10+
render,
11+
setupBeforeEach,
12+
} from "../__fixtures__/testUtils";
13+
14+
let handlers: Handlers = {
15+
onStart: jest.fn(),
16+
onMove: jest.fn(),
17+
onInteractive: jest.fn(),
18+
onEnd: jest.fn(),
19+
};
20+
21+
jest.mock("../../../../hooks", () => ({
22+
useKeyboardHandler: jest.fn((h: Handlers) => {
23+
handlers = h;
24+
}),
25+
useResizeMode: jest.fn(),
26+
}));
27+
28+
jest.mock("../../../hooks/useScrollState", () => ({
29+
__esModule: true,
30+
default: jest.fn(() => ({
31+
offset: mockOffset,
32+
layout: mockLayout,
33+
size: mockSize,
34+
})),
35+
}));
36+
37+
beforeEach(() => {
38+
setupBeforeEach();
39+
Object.defineProperty(Platform, "OS", { value: "android" });
40+
});
41+
42+
afterAll(() => {
43+
Object.defineProperty(Platform, "OS", { value: "ios" });
44+
});
45+
46+
describe("`useChatKeyboard` — Android behaviors", () => {
47+
it("persistent inverted: should NOT reset padding on close", () => {
48+
const { result } = render({
49+
inverted: true,
50+
keyboardLiftBehavior: "persistent",
51+
});
52+
53+
handlers.onStart({ height: KEYBOARD });
54+
handlers.onMove({ height: KEYBOARD });
55+
handlers.onEnd({ height: 0 });
56+
57+
expect(result.current.padding.value).toBe(KEYBOARD);
58+
});
59+
60+
it("persistent inverted: should not decrease shift", () => {
61+
render({
62+
inverted: true,
63+
keyboardLiftBehavior: "persistent",
64+
});
65+
66+
handlers.onStart({ height: KEYBOARD });
67+
handlers.onMove({ height: KEYBOARD });
68+
expect(mockScrollTo).toHaveBeenCalled();
69+
mockScrollTo.mockClear();
70+
71+
// currentShift = offsetBeforeScroll(0) + padding(300) - scroll(0) = 300
72+
// effective = 200 < 300 → return
73+
handlers.onMove({ height: 200 });
74+
expect(mockScrollTo).not.toHaveBeenCalled();
75+
});
76+
77+
it("never non-inverted: should not scroll", () => {
78+
mockOffset.value = 100;
79+
render({ inverted: false, keyboardLiftBehavior: "never" });
80+
81+
handlers.onStart({ height: KEYBOARD });
82+
handlers.onMove({ height: 200 });
83+
84+
expect(mockScrollTo).not.toHaveBeenCalled();
85+
});
86+
87+
it("never inverted: should not scroll", () => {
88+
render({ inverted: true, keyboardLiftBehavior: "never" });
89+
90+
handlers.onStart({ height: KEYBOARD });
91+
handlers.onMove({ height: 200 });
92+
93+
expect(mockScrollTo).not.toHaveBeenCalled();
94+
});
95+
96+
it("whenAtEnd non-inverted: should scroll when at end", () => {
97+
mockOffset.value = 1180;
98+
render({ inverted: false, keyboardLiftBehavior: "whenAtEnd" });
99+
100+
handlers.onStart({ height: KEYBOARD });
101+
handlers.onMove({ height: 200 });
102+
103+
expect(mockScrollTo).toHaveBeenCalled();
104+
});
105+
106+
it("whenAtEnd non-inverted: should NOT scroll when NOT at end", () => {
107+
mockOffset.value = 100;
108+
render({ inverted: false, keyboardLiftBehavior: "whenAtEnd" });
109+
110+
handlers.onStart({ height: KEYBOARD });
111+
handlers.onMove({ height: 200 });
112+
113+
expect(mockScrollTo).not.toHaveBeenCalled();
114+
});
115+
116+
it("whenAtEnd inverted: should scroll when at end (offset near 0)", () => {
117+
mockOffset.value = 0;
118+
render({ inverted: true, keyboardLiftBehavior: "whenAtEnd" });
119+
120+
handlers.onStart({ height: KEYBOARD });
121+
handlers.onMove({ height: 200 });
122+
123+
expect(mockScrollTo).toHaveBeenCalled();
124+
});
125+
126+
it("whenAtEnd inverted: should NOT scroll when scrolled away", () => {
127+
mockOffset.value = 500;
128+
render({ inverted: true, keyboardLiftBehavior: "whenAtEnd" });
129+
130+
handlers.onStart({ height: KEYBOARD });
131+
handlers.onMove({ height: 200 });
132+
133+
expect(mockScrollTo).not.toHaveBeenCalled();
134+
});
135+
136+
it("persistent non-inverted: should not scroll back on shrink", () => {
137+
mockOffset.value = 100;
138+
render({ inverted: false, keyboardLiftBehavior: "persistent" });
139+
140+
handlers.onStart({ height: KEYBOARD });
141+
handlers.onMove({ height: 200 });
142+
expect(mockScrollTo).toHaveBeenCalled();
143+
mockScrollTo.mockClear();
144+
145+
mockOffset.value = 300;
146+
handlers.onMove({ height: 100 });
147+
148+
expect(mockScrollTo).not.toHaveBeenCalled();
149+
});
150+
});

src/components/KeyboardChatScrollView/useChatKeyboard/__tests__/freeze.android.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ describe("`useChatKeyboard` — Android freeze", () => {
5858
expect(mockScrollTo).not.toHaveBeenCalled();
5959
});
6060

61-
it("should not change containerTranslateY on keyboard open", () => {
61+
it("should not change padding on inverted keyboard open", () => {
6262
const { result } = render({
6363
inverted: true,
6464
keyboardLiftBehavior: "always",
@@ -68,7 +68,7 @@ describe("`useChatKeyboard` — Android freeze", () => {
6868
handlers.onStart({ height: KEYBOARD });
6969
handlers.onMove({ height: 200 });
7070

71-
expect(result.current.containerTranslateY.value).toBe(0);
71+
expect(result.current.padding.value).toBe(0);
7272
});
7373

7474
it("should not change padding in onEnd", () => {

src/components/KeyboardChatScrollView/useChatKeyboard/__tests__/index.android.spec.ts

Lines changed: 68 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -162,130 +162,114 @@ describe("`useChatKeyboard` — Android non-inverted + always", () => {
162162
});
163163

164164
describe("`useChatKeyboard` — Android inverted + always", () => {
165-
it("should set containerTranslateY in onMove", () => {
165+
it("should set padding in onStart and call scrollTo in onMove", () => {
166166
const { result } = render({
167167
inverted: true,
168168
keyboardLiftBehavior: "always",
169169
});
170170

171171
handlers.onStart({ height: KEYBOARD });
172-
handlers.onMove({ height: 200 });
172+
expect(result.current.padding.value).toBe(KEYBOARD);
173173

174-
expect(result.current.containerTranslateY.value).toBe(-200);
174+
handlers.onMove({ height: 200 });
175+
// target = offsetBeforeScroll(0) + padding(300) - effective(200) = 100
176+
expect(mockScrollTo).toHaveBeenCalledWith(expect.anything(), 0, 100, false);
175177
});
176178

177-
it("should update containerTranslateY per-frame", () => {
178-
const { result } = render({
179-
inverted: true,
180-
keyboardLiftBehavior: "always",
181-
});
179+
it("should call scrollTo per-frame with correct targets", () => {
180+
render({ inverted: true, keyboardLiftBehavior: "always" });
182181

183182
handlers.onStart({ height: KEYBOARD });
184183

185184
handlers.onMove({ height: 100 });
186-
expect(result.current.containerTranslateY.value).toBe(-100);
185+
// target = 0 + 300 - 100 = 200
186+
expect(mockScrollTo).toHaveBeenLastCalledWith(
187+
expect.anything(),
188+
0,
189+
200,
190+
false,
191+
);
187192

188193
handlers.onMove({ height: 250 });
189-
expect(result.current.containerTranslateY.value).toBe(-250);
190-
});
191-
192-
it("should NOT call scrollTo for inverted lists", () => {
193-
render({ inverted: true, keyboardLiftBehavior: "always" });
194-
195-
handlers.onStart({ height: KEYBOARD });
196-
handlers.onMove({ height: 200 });
197-
198-
expect(mockScrollTo).not.toHaveBeenCalled();
194+
// target = 0 + 300 - 250 = 50
195+
expect(mockScrollTo).toHaveBeenLastCalledWith(
196+
expect.anything(),
197+
0,
198+
50,
199+
false,
200+
);
199201
});
200202

201-
it("should reset containerTranslateY on keyboard close", () => {
203+
it("should return undefined for contentOffsetY", () => {
202204
const { result } = render({
203205
inverted: true,
204206
keyboardLiftBehavior: "always",
205207
});
206208

207-
handlers.onStart({ height: KEYBOARD });
208-
handlers.onMove({ height: KEYBOARD });
209-
handlers.onEnd({ height: 0 });
210-
211-
expect(result.current.containerTranslateY.value).toBe(0);
212-
expect(result.current.padding.value).toBe(0);
213-
});
214-
});
215-
216-
describe("`useChatKeyboard` — Android behaviors", () => {
217-
it("persistent inverted: should NOT reset translateY on close", () => {
218-
const { result } = render({
219-
inverted: true,
220-
keyboardLiftBehavior: "persistent",
221-
});
222-
223-
handlers.onStart({ height: KEYBOARD });
224-
handlers.onMove({ height: KEYBOARD });
225-
handlers.onEnd({ height: 0 });
226-
227-
expect(result.current.containerTranslateY.value).toBe(-KEYBOARD);
209+
expect(result.current.contentOffsetY).toBeUndefined();
228210
});
229211

230-
it("persistent inverted: should not shrink translateY", () => {
231-
const { result } = render({
232-
inverted: true,
233-
keyboardLiftBehavior: "persistent",
234-
});
212+
it("should scroll back on keyboard close", () => {
213+
render({ inverted: true, keyboardLiftBehavior: "always" });
235214

236215
handlers.onStart({ height: KEYBOARD });
237216
handlers.onMove({ height: KEYBOARD });
238-
handlers.onMove({ height: 200 });
239-
240-
expect(result.current.containerTranslateY.value).toBe(-KEYBOARD);
241-
});
217+
mockScrollTo.mockClear();
242218

243-
it("never: should not scroll or translate", () => {
244-
mockOffset.value = 100;
245-
const { result } = render({
246-
inverted: false,
247-
keyboardLiftBehavior: "never",
248-
});
219+
// keyboard closes: offsetBeforeScroll = scroll.value = 0
220+
handlers.onStart({ height: 0 });
249221

250-
handlers.onStart({ height: KEYBOARD });
251-
handlers.onMove({ height: 200 });
222+
handlers.onMove({ height: 150 });
223+
// target = 0 + 300 - 150 = 150
224+
expect(mockScrollTo).toHaveBeenLastCalledWith(
225+
expect.anything(),
226+
0,
227+
150,
228+
false,
229+
);
252230

253-
expect(mockScrollTo).not.toHaveBeenCalled();
254-
expect(result.current.containerTranslateY.value).toBe(0);
231+
handlers.onMove({ height: 0 });
232+
// target = 0 + 300 - 0 = 300
233+
expect(mockScrollTo).toHaveBeenLastCalledWith(
234+
expect.anything(),
235+
0,
236+
300,
237+
false,
238+
);
255239
});
256240

257-
it("whenAtEnd: should scroll when at end", () => {
258-
mockOffset.value = 1180;
259-
render({ inverted: false, keyboardLiftBehavior: "whenAtEnd" });
241+
it("should preserve user scroll position on close", () => {
242+
render({ inverted: true, keyboardLiftBehavior: "always" });
260243

261244
handlers.onStart({ height: KEYBOARD });
262-
handlers.onMove({ height: 200 });
263-
264-
expect(mockScrollTo).toHaveBeenCalled();
265-
});
266-
267-
it("whenAtEnd: should NOT scroll when NOT at end", () => {
268-
mockOffset.value = 100;
269-
render({ inverted: false, keyboardLiftBehavior: "whenAtEnd" });
245+
handlers.onMove({ height: KEYBOARD });
246+
mockScrollTo.mockClear();
270247

271-
handlers.onStart({ height: KEYBOARD });
272-
handlers.onMove({ height: 200 });
248+
// user scrolled to 50 while keyboard open
249+
mockOffset.value = 50;
250+
handlers.onStart({ height: 0 });
273251

274-
expect(mockScrollTo).not.toHaveBeenCalled();
252+
handlers.onMove({ height: 150 });
253+
// target = 50 + 300 - 150 = 200
254+
expect(mockScrollTo).toHaveBeenLastCalledWith(
255+
expect.anything(),
256+
0,
257+
200,
258+
false,
259+
);
275260
});
276261

277-
it("persistent non-inverted: should not scroll back on shrink", () => {
278-
mockOffset.value = 100;
279-
render({ inverted: false, keyboardLiftBehavior: "persistent" });
262+
it("should finalize padding in onEnd", () => {
263+
const { result } = render({
264+
inverted: true,
265+
keyboardLiftBehavior: "always",
266+
});
280267

281268
handlers.onStart({ height: KEYBOARD });
282-
handlers.onMove({ height: 200 });
283-
expect(mockScrollTo).toHaveBeenCalled();
284-
mockScrollTo.mockClear();
285-
286-
mockOffset.value = 300;
287-
handlers.onMove({ height: 100 });
269+
handlers.onEnd({ height: KEYBOARD });
270+
expect(result.current.padding.value).toBe(KEYBOARD);
288271

289-
expect(mockScrollTo).not.toHaveBeenCalled();
272+
handlers.onEnd({ height: 0 });
273+
expect(result.current.padding.value).toBe(0);
290274
});
291275
});

0 commit comments

Comments
 (0)