Skip to content

Commit cd114d7

Browse files
swap out the old library with react-native-keyboard-controller
1 parent 7ae0360 commit cd114d7

File tree

8 files changed

+113
-73
lines changed

8 files changed

+113
-73
lines changed

packages/components-native/src/Form/Form.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,14 @@ const onChangeMock = jest.fn();
3232
const onChangeSelectMock = jest.fn();
3333
const onChangeSwitchMock = jest.fn();
3434

35-
const mockScrollToPosition = jest.fn();
35+
const mockScrollTo = jest.fn();
3636
const mockScrollToTop = jest.fn();
3737

3838
jest.mock("./hooks/useFormViewRefs", () => ({
3939
useFormViewRefs: () => {
4040
return {
4141
scrollViewRef: {
42-
current: { scrollToPosition: mockScrollToPosition },
42+
current: { scrollTo: mockScrollTo },
4343
},
4444
bottomViewRef: { current: {} },
4545
scrollToTop: mockScrollToTop,

packages/components-native/src/Form/Form.tsx

Lines changed: 53 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1-
import React, { useState } from "react";
1+
import React, { useCallback, useEffect, useState } from "react";
22
import type { FieldValues } from "react-hook-form";
33
import { FormProvider } from "react-hook-form";
4-
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
4+
import {
5+
KeyboardAwareScrollView,
6+
KeyboardEvents,
7+
} from "react-native-keyboard-controller";
8+
import type { KeyboardEventData } from "react-native-keyboard-controller";
59
import type { LayoutChangeEvent } from "react-native";
610
import { Keyboard, Platform, View, findNodeHandle } from "react-native";
711
import { useStyles } from "./Form.style";
@@ -117,21 +121,54 @@ function InternalForm<T extends FieldValues, S>({
117121
formState: formMethods.formState,
118122
refNode: findNodeHandle(scrollViewRef.current),
119123
setFocus: formMethods.setFocus,
120-
scrollToPosition: scrollViewRef.current?.scrollToPosition,
124+
scrollTo: scrollViewRef.current?.scrollTo,
121125
});
122126

123127
const handleOfflineSubmit = useOfflineHandler();
124128

125-
const keyboardProps = Platform.select({
126-
ios: {
127-
onKeyboardWillHide: handleKeyboardHide,
128-
onKeyboardWillShow: handleKeyboardShow,
129+
const handleKeyboardShow = useCallback(
130+
(event: KeyboardEventData) => {
131+
setKeyboardHeight(event.height);
132+
setKeyboardScreenY(windowHeight - event.height);
129133
},
130-
android: {
131-
onKeyboardDidHide: handleKeyboardHide,
132-
onKeyboardDidShow: handleKeyboardShow,
133-
},
134-
});
134+
[windowHeight],
135+
);
136+
137+
const handleKeyboardHide = useCallback(() => {
138+
bottomViewRef?.current?.measureInWindow(
139+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
140+
(_x: number, y: number, _width: number, _height: number) => {
141+
// This fixes extra whitespace below the form if it was scrolled down while the keyboard was open
142+
// i.e. a View below the form is higher than the bottom of the window
143+
if (y < windowHeight) {
144+
scrollViewRef?.current?.scrollToEnd();
145+
}
146+
},
147+
);
148+
setKeyboardHeight(0);
149+
setKeyboardScreenY(0);
150+
}, [bottomViewRef, scrollViewRef, windowHeight]);
151+
152+
useEffect(() => {
153+
const showEvent =
154+
Platform.OS === "ios" ? "keyboardWillShow" : "keyboardDidShow";
155+
const hideEvent =
156+
Platform.OS === "ios" ? "keyboardWillHide" : "keyboardDidHide";
157+
158+
const showListener = KeyboardEvents.addListener(
159+
showEvent,
160+
handleKeyboardShow,
161+
);
162+
const hideListener = KeyboardEvents.addListener(
163+
hideEvent,
164+
handleKeyboardHide,
165+
);
166+
167+
return () => {
168+
showListener.remove();
169+
hideListener.remove();
170+
};
171+
}, [handleKeyboardHide, handleKeyboardShow]);
135172

136173
const onLayout = (event: LayoutChangeEvent) => {
137174
setMessageBannerHeight(event.nativeEvent.layout.height);
@@ -166,17 +203,12 @@ function InternalForm<T extends FieldValues, S>({
166203
saveButtonOffset={saveButtonOffset}
167204
>
168205
<KeyboardAwareScrollView
169-
enableResetScrollToCoords={false}
170-
enableAutomaticScroll={!disableKeyboardAwareScroll}
171-
enableOnAndroid={edgeToEdgeEnabled}
172-
keyboardOpeningTime={
173-
Platform.OS === "ios" ? tokens["timing-slowest"] : 0
174-
}
206+
disableScrollOnKeyboardHide={true}
207+
enabled={!disableKeyboardAwareScroll}
175208
keyboardShouldPersistTaps={"handled"}
176209
ref={scrollViewRef}
177-
{...keyboardProps}
178-
extraHeight={headerHeight}
179-
extraScrollHeight={edgeToEdgeEnabled ? tokens["space-large"] : 0}
210+
bottomOffset={headerHeight}
211+
extraKeyboardSpace={edgeToEdgeEnabled ? tokens["space-large"] : 0}
180212
contentContainerStyle={
181213
!keyboardHeight && styles.scrollContentContainer
182214
}
@@ -235,26 +267,6 @@ function InternalForm<T extends FieldValues, S>({
235267
);
236268

237269
// eslint-disable-next-line @typescript-eslint/no-explicit-any
238-
function handleKeyboardShow(frames: Record<string, any>) {
239-
setKeyboardScreenY(frames.endCoordinates.screenY);
240-
setKeyboardHeight(frames.endCoordinates.height);
241-
}
242-
243-
function handleKeyboardHide() {
244-
bottomViewRef?.current?.measureInWindow(
245-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
246-
(_x: number, y: number, _width: number, _height: number) => {
247-
// This fixes extra whitespace below the form if it was scrolled down while the keyboard was open
248-
// i.e. a View below the form is higher than the bottom of the window
249-
if (y < windowHeight) {
250-
scrollViewRef?.current?.scrollToEnd();
251-
}
252-
},
253-
);
254-
setKeyboardHeight(0);
255-
setKeyboardScreenY(0);
256-
}
257-
258270
async function internalSubmit(data: FormValues<T>) {
259271
let performSubmit = true;
260272

packages/components-native/src/Form/hooks/useFormViewRefs.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
import type { RefObject } from "react";
22
import { useCallback, useRef } from "react";
33
import type { View } from "react-native";
4-
import type { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
4+
import type { KeyboardAwareScrollViewRef } from "react-native-keyboard-controller";
55

66
interface UseFormViewRefsReturn {
7-
readonly scrollViewRef: RefObject<KeyboardAwareScrollView | null>;
7+
readonly scrollViewRef: RefObject<KeyboardAwareScrollViewRef | null>;
88
readonly bottomViewRef: RefObject<View | null>;
99
readonly scrollToTop: () => void;
1010
}
1111

1212
export function useFormViewRefs(): UseFormViewRefsReturn {
13-
const scrollViewRef = useRef<KeyboardAwareScrollView>(null);
13+
const scrollViewRef = useRef<KeyboardAwareScrollViewRef>(null);
1414
const bottomViewRef = useRef<View>(null);
1515
const scrollToTop = useCallback(() => {
16-
scrollViewRef.current?.scrollToPosition(0, 0);
16+
scrollViewRef.current?.scrollTo({ x: 0, y: 0, animated: true });
1717
}, [scrollViewRef]);
1818

1919
return {

packages/components-native/src/Form/hooks/useInternalForm.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type {
66
} from "react-hook-form";
77
import { useForm } from "react-hook-form";
88
import type { RefObject } from "react";
9-
import type { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
9+
import type { KeyboardAwareScrollViewRef } from "react-native-keyboard-controller";
1010
import { useAtlantisContext } from "../../AtlantisContext";
1111
import { useAtlantisFormContext } from "../context/AtlantisFormContext";
1212
import type { InternalFormProps } from "../types";
@@ -22,7 +22,7 @@ type UseInternalFormProps<T extends FieldValues, SubmitResponseType> = Pick<
2222
| "localCacheId"
2323
| "UNSAFE_allowDiscardLocalCacheWhenOffline"
2424
> & {
25-
scrollViewRef?: RefObject<KeyboardAwareScrollView | null>;
25+
scrollViewRef?: RefObject<KeyboardAwareScrollViewRef | null>;
2626
readonly saveButtonHeight: number;
2727
readonly messageBannerHeight: number;
2828
};

packages/components-native/src/Form/hooks/useScrollToError/useScrollToError.test.tsx

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
import { renderHook } from "@testing-library/react-native";
2+
import type { FieldValues, FormState } from "react-hook-form";
23
import { useScrollToError } from "./useScrollToError";
4+
import type { UseScrollToErrorParams } from "./useScrollToError";
35

4-
const mockFormState = {
6+
const mockFormState: FormState<FieldValues> = {
57
isDirty: true,
8+
isLoading: false,
9+
isReady: true,
10+
disabled: false,
11+
validatingFields: {},
612
dirtyFields: {},
713
isSubmitted: false,
814
isSubmitSuccessful: false,
@@ -33,38 +39,47 @@ jest.mock("../../../ErrorMessageWrapper", () => ({
3339
}),
3440
}));
3541

36-
const handleScrollToPosition = jest.fn();
42+
const handleScrollTo = jest.fn();
3743
const handleSetFocus = jest.fn();
3844

39-
const initialProps = {
45+
type UseScrollToErrorProps = UseScrollToErrorParams<FieldValues>;
46+
47+
const initialProps: UseScrollToErrorProps = {
4048
formState: mockFormState,
4149
refNode: 1,
42-
scrollToPosition: handleScrollToPosition,
50+
scrollTo: handleScrollTo,
4351
setFocus: handleSetFocus,
4452
};
4553

4654
afterEach(() => {
47-
handleScrollToPosition.mockClear();
55+
handleScrollTo.mockClear();
4856
handleSetFocus.mockClear();
4957
});
5058

5159
describe("useScrollToError", () => {
5260
it("should do nothing if everything is valid", () => {
53-
renderHook(useScrollToError, { initialProps });
61+
renderHook((props: UseScrollToErrorProps) => useScrollToError(props), {
62+
initialProps,
63+
});
5464

5565
expect(handleSetFocus).not.toHaveBeenCalled();
56-
expect(handleScrollToPosition).not.toHaveBeenCalled();
66+
expect(handleScrollTo).not.toHaveBeenCalled();
5767
});
5868

5969
it("should focus with RHF if it can", () => {
60-
const { rerender } = renderHook(useScrollToError, { initialProps });
70+
const { rerender } = renderHook(
71+
(props: UseScrollToErrorProps) => useScrollToError(props),
72+
{
73+
initialProps,
74+
},
75+
);
6176
rerender({
6277
...initialProps,
6378
formState: { ...mockFormState, isValid: false, submitCount: 1 },
6479
});
6580

6681
expect(handleSetFocus).toHaveBeenCalled();
67-
expect(handleScrollToPosition).not.toHaveBeenCalled();
82+
expect(handleScrollTo).not.toHaveBeenCalled();
6883
});
6984

7085
it("should manually scroll", () => {
@@ -74,31 +89,39 @@ describe("useScrollToError", () => {
7489
const failedSetFocus = jest.fn(() => failingFn());
7590
const manualScrollProps = { ...initialProps, setFocus: failedSetFocus };
7691

77-
const { rerender } = renderHook(useScrollToError, {
78-
initialProps: manualScrollProps,
79-
});
92+
const { rerender } = renderHook(
93+
(props: UseScrollToErrorProps) => useScrollToError(props),
94+
{
95+
initialProps: manualScrollProps,
96+
},
97+
);
8098
rerender({
8199
...manualScrollProps,
82100
formState: { ...mockFormState, isValid: false, submitCount: 1 },
83101
});
84102

85103
expect(failedSetFocus).toHaveBeenCalled();
86104
expect(failedSetFocus).toThrow();
87-
expect(handleScrollToPosition).toHaveBeenCalled();
105+
expect(handleScrollTo).toHaveBeenCalled();
88106
});
89107

90108
describe("With screen readers", () => {
91109
it("should not fire the setFocus", () => {
92110
mockScreenReaderEnabled.mockReturnValue(true);
93111

94-
const { rerender } = renderHook(useScrollToError, { initialProps });
112+
const { rerender } = renderHook(
113+
(props: UseScrollToErrorProps) => useScrollToError(props),
114+
{
115+
initialProps,
116+
},
117+
);
95118
rerender({
96119
...initialProps,
97120
formState: { ...mockFormState, isValid: false, submitCount: 1 },
98121
});
99122

100123
expect(handleSetFocus).not.toHaveBeenCalled();
101-
expect(handleScrollToPosition).toHaveBeenCalled();
124+
expect(handleScrollTo).toHaveBeenCalled();
102125
});
103126
});
104127
});

packages/components-native/src/Form/hooks/useScrollToError/useScrollToError.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,24 @@ import type {
55
Path,
66
UseFormSetFocus,
77
} from "react-hook-form";
8-
import type { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
98
import type { MeasureInWindowOnSuccessCallback } from "react-native";
9+
import type { KeyboardAwareScrollViewRef } from "react-native-keyboard-controller";
1010
import { Keyboard, Platform } from "react-native";
1111
import { useIsScreenReaderEnabled } from "../../../hooks";
1212
import { useErrorMessageContext } from "../../../ErrorMessageWrapper";
1313

14-
interface UseScrollToErrorParams<T extends FieldValues> {
14+
export interface UseScrollToErrorParams<T extends FieldValues> {
1515
readonly formState: FormState<T>;
1616
readonly refNode: number | null;
1717
readonly setFocus: UseFormSetFocus<T>;
18-
readonly scrollToPosition?: KeyboardAwareScrollView["scrollToPosition"];
18+
readonly scrollTo?: KeyboardAwareScrollViewRef["scrollTo"];
1919
}
2020

2121
export function useScrollToError<T extends FieldValues>({
2222
formState: { errors, isValid, submitCount },
2323
refNode,
2424
setFocus,
25-
scrollToPosition,
25+
scrollTo,
2626
}: UseScrollToErrorParams<T>): void {
2727
const [submitCounter, setSubmitCounter] = useState(submitCount);
2828
const isScreenReaderEnabled = useIsScreenReaderEnabled();
@@ -66,7 +66,7 @@ export function useScrollToError<T extends FieldValues>({
6666
isScreenReaderEnabled && Platform.OS === "android";
6767
const shouldAnimateScroll = !isAndroidWithScreenReader;
6868

69-
scrollToPosition?.(x, y, shouldAnimateScroll);
69+
scrollTo?.({ x, y, animated: shouldAnimateScroll });
7070
}
7171
}
7272

packages/components-native/src/Form/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import type {
88
UseFormReturn,
99
} from "react-hook-form";
1010
import type { IconNames } from "@jobber/design";
11-
import type { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
11+
import type { KeyboardAwareScrollViewRef } from "react-native-keyboard-controller";
1212

1313
export type FormValues<T> = T;
1414
export type FormErrors = FormNetworkErrors | FormUserErrors;
@@ -54,7 +54,7 @@ interface FormNoticeMessage {
5454

5555
export type FormRef<T extends FieldValues = FieldValues> =
5656
| (UseFormReturn<T> & {
57-
scrollViewRef?: RefObject<KeyboardAwareScrollView | null>;
57+
scrollViewRef?: RefObject<KeyboardAwareScrollViewRef | null>;
5858
saveButtonHeight?: number;
5959
messageBannerHeight?: number;
6060
})

packages/components-native/src/__mocks__/__mocks.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,13 @@ jest.doMock("react-native", () => {
7979
// eslint-disable-next-line @typescript-eslint/no-unused-vars
8080
const KeyboardAwareScrollView = ({ children }, _ref) => children;
8181
const mockRef = React.forwardRef(KeyboardAwareScrollView);
82-
jest.mock("react-native-keyboard-aware-scroll-view", () => {
83-
return { KeyboardAwareScrollView: mockRef };
82+
jest.mock("react-native-keyboard-controller", () => {
83+
return {
84+
KeyboardAwareScrollView: mockRef,
85+
KeyboardEvents: {
86+
addListener: jest.fn(() => ({ remove: jest.fn() })),
87+
},
88+
};
8489
});
8590

8691
jest.mock("react-native-modalize", () => {

0 commit comments

Comments
 (0)