@jobber/components-native 0.99.0 → 0.100.1

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.
Files changed (42) hide show
  1. package/dist/package.json +4 -6
  2. package/dist/src/Button/Button.js +2 -2
  3. package/dist/src/ContentOverlay/BottomSheetKeyboardAwareScrollView.js +19 -0
  4. package/dist/src/ContentOverlay/ContentOverlay.js +143 -107
  5. package/dist/src/ContentOverlay/ContentOverlay.style.js +8 -12
  6. package/dist/src/ContentOverlay/computeContentOverlayBehavior.js +76 -0
  7. package/dist/src/ContentOverlay/constants.js +1 -0
  8. package/dist/src/ContentOverlay/hooks/useBottomSheetModalBackHandler.js +25 -0
  9. package/dist/src/ContentOverlay/index.js +1 -1
  10. package/dist/src/InputText/InputText.js +44 -1
  11. package/dist/tsconfig.build.tsbuildinfo +1 -1
  12. package/dist/types/src/ActionLabel/ActionLabel.d.ts +1 -1
  13. package/dist/types/src/ContentOverlay/BottomSheetKeyboardAwareScrollView.d.ts +11 -0
  14. package/dist/types/src/ContentOverlay/ContentOverlay.d.ts +2 -5
  15. package/dist/types/src/ContentOverlay/ContentOverlay.style.d.ts +11 -10
  16. package/dist/types/src/ContentOverlay/computeContentOverlayBehavior.d.ts +32 -0
  17. package/dist/types/src/ContentOverlay/constants.d.ts +1 -0
  18. package/dist/types/src/ContentOverlay/hooks/useBottomSheetModalBackHandler.d.ts +7 -0
  19. package/dist/types/src/ContentOverlay/index.d.ts +1 -1
  20. package/dist/types/src/ContentOverlay/types.d.ts +5 -12
  21. package/jestSetup.js +2 -0
  22. package/package.json +4 -6
  23. package/src/ActionLabel/ActionLabel.test.tsx +13 -1
  24. package/src/ActionLabel/ActionLabel.tsx +6 -1
  25. package/src/Button/Button.tsx +2 -2
  26. package/src/ContentOverlay/BottomSheetKeyboardAwareScrollView.tsx +36 -0
  27. package/src/ContentOverlay/ContentOverlay.stories.tsx +32 -36
  28. package/src/ContentOverlay/ContentOverlay.style.ts +12 -12
  29. package/src/ContentOverlay/ContentOverlay.test.tsx +157 -79
  30. package/src/ContentOverlay/ContentOverlay.tsx +247 -205
  31. package/src/ContentOverlay/computeContentOverlayBehavior.test.ts +276 -0
  32. package/src/ContentOverlay/computeContentOverlayBehavior.ts +119 -0
  33. package/src/ContentOverlay/constants.ts +1 -0
  34. package/src/ContentOverlay/hooks/useBottomSheetModalBackHandler.test.ts +81 -0
  35. package/src/ContentOverlay/hooks/useBottomSheetModalBackHandler.ts +36 -0
  36. package/src/ContentOverlay/index.ts +4 -1
  37. package/src/ContentOverlay/types.ts +5 -13
  38. package/src/InputText/InputText.test.tsx +122 -0
  39. package/src/InputText/InputText.tsx +62 -2
  40. package/dist/src/ContentOverlay/UNSAFE_WrappedModalize.js +0 -23
  41. package/dist/types/src/ContentOverlay/UNSAFE_WrappedModalize.d.ts +0 -3
  42. package/src/ContentOverlay/UNSAFE_WrappedModalize.tsx +0 -41
@@ -0,0 +1,276 @@
1
+ import type {
2
+ ContentOverlayConfig,
3
+ ContentOverlayState,
4
+ } from "./computeContentOverlayBehavior";
5
+ import { computeContentOverlayBehavior } from "./computeContentOverlayBehavior";
6
+
7
+ const arbitraryClosedPositionValue = 768;
8
+
9
+ const defaultConfig: ContentOverlayConfig = {
10
+ fullScreen: false,
11
+ adjustToContentHeight: false,
12
+ isDraggable: true,
13
+ hasOnBeforeExit: false,
14
+ showDismiss: false,
15
+ };
16
+
17
+ const defaultState: ContentOverlayState = {
18
+ isScreenReaderEnabled: false,
19
+ position: arbitraryClosedPositionValue,
20
+ };
21
+
22
+ function aConfig(
23
+ overrides: Partial<ContentOverlayConfig> = {},
24
+ ): ContentOverlayConfig {
25
+ return {
26
+ ...defaultConfig,
27
+ ...overrides,
28
+ };
29
+ }
30
+
31
+ function aState(
32
+ overrides: Partial<ContentOverlayState> = {},
33
+ ): ContentOverlayState {
34
+ return {
35
+ ...defaultState,
36
+ ...overrides,
37
+ };
38
+ }
39
+
40
+ describe("computeContentOverlayBehavior", () => {
41
+ describe("initialHeight", () => {
42
+ it("returns fullScreen when fullScreen=true", () => {
43
+ const config = aConfig({ fullScreen: true });
44
+ const state = aState();
45
+
46
+ const result = computeContentOverlayBehavior(config, state);
47
+
48
+ expect(result.initialHeight).toBe("fullScreen");
49
+ });
50
+
51
+ it("returns contentHeight when adjustToContentHeight=true", () => {
52
+ const config = aConfig({ adjustToContentHeight: true });
53
+ const state = aState();
54
+
55
+ const result = computeContentOverlayBehavior(config, state);
56
+
57
+ expect(result.initialHeight).toBe("contentHeight");
58
+ });
59
+
60
+ it("returns contentHeight for default props (legacy case)", () => {
61
+ const config = aConfig();
62
+ const state = aState();
63
+
64
+ const result = computeContentOverlayBehavior(config, state);
65
+
66
+ expect(result.initialHeight).toBe("contentHeight");
67
+ });
68
+
69
+ it("returns adjustToContentHeight when adjustToContentHeight=true even if fullScreen=true", () => {
70
+ const config = aConfig({ fullScreen: true, adjustToContentHeight: true });
71
+ const state = aState();
72
+
73
+ const result = computeContentOverlayBehavior(config, state);
74
+
75
+ expect(result.initialHeight).toBe("contentHeight");
76
+ });
77
+ });
78
+
79
+ describe("isDraggable", () => {
80
+ it("returns false when onBeforeExit is present regardless of isDraggable prop", () => {
81
+ const config = aConfig({ isDraggable: true, hasOnBeforeExit: true });
82
+ const state = aState();
83
+
84
+ const result = computeContentOverlayBehavior(config, state);
85
+
86
+ expect(result.isDraggable).toBe(false);
87
+ });
88
+
89
+ it("returns false when onBeforeExit is present and isDraggable is false", () => {
90
+ const config = aConfig({ isDraggable: false, hasOnBeforeExit: true });
91
+ const state = aState();
92
+
93
+ const result = computeContentOverlayBehavior(config, state);
94
+
95
+ expect(result.isDraggable).toBe(false);
96
+ });
97
+
98
+ it("respects isDraggable=true when no onBeforeExit", () => {
99
+ const config = aConfig({ isDraggable: true, hasOnBeforeExit: false });
100
+ const state = aState();
101
+
102
+ const result = computeContentOverlayBehavior(config, state);
103
+
104
+ expect(result.isDraggable).toBe(true);
105
+ });
106
+
107
+ it("respects isDraggable=false when no onBeforeExit", () => {
108
+ const config = aConfig({ isDraggable: false, hasOnBeforeExit: false });
109
+ const state = aState();
110
+
111
+ const result = computeContentOverlayBehavior(config, state);
112
+
113
+ expect(result.isDraggable).toBe(false);
114
+ });
115
+ });
116
+
117
+ describe("showDismiss", () => {
118
+ it("returns true when showDismiss prop is true", () => {
119
+ const config = aConfig({ showDismiss: true });
120
+ const state = aState();
121
+
122
+ const result = computeContentOverlayBehavior(config, state);
123
+
124
+ expect(result.showDismiss).toBe(true);
125
+ });
126
+
127
+ it("returns true when screen reader is enabled", () => {
128
+ const config = aConfig({ showDismiss: false });
129
+ const state = aState({ isScreenReaderEnabled: true });
130
+
131
+ const result = computeContentOverlayBehavior(config, state);
132
+
133
+ expect(result.showDismiss).toBe(true);
134
+ });
135
+
136
+ it("returns true when fullScreen is true", () => {
137
+ const config = aConfig({ fullScreen: true, showDismiss: false });
138
+ const state = aState({ isScreenReaderEnabled: false });
139
+
140
+ const result = computeContentOverlayBehavior(config, state);
141
+
142
+ expect(result.showDismiss).toBe(true);
143
+ });
144
+
145
+ it("returns true when dragged to top and not adjustToContentHeight (legacy behavior)", () => {
146
+ const config = aConfig({
147
+ adjustToContentHeight: false,
148
+ showDismiss: false,
149
+ });
150
+ const state = aState({ position: 0, isScreenReaderEnabled: false });
151
+
152
+ const result = computeContentOverlayBehavior(config, state);
153
+
154
+ expect(result.showDismiss).toBe(true);
155
+ });
156
+
157
+ it("returns false when position is top but adjustToContentHeight is true", () => {
158
+ const config = aConfig({
159
+ adjustToContentHeight: true,
160
+ showDismiss: false,
161
+ });
162
+ const state = aState({ position: 0, isScreenReaderEnabled: false });
163
+
164
+ const result = computeContentOverlayBehavior(config, state);
165
+
166
+ expect(result.showDismiss).toBe(false);
167
+ });
168
+
169
+ it("returns false when position is initial and no other conditions met", () => {
170
+ const config = aConfig({
171
+ showDismiss: false,
172
+ fullScreen: false,
173
+ adjustToContentHeight: false,
174
+ });
175
+ const state = aState({
176
+ position: arbitraryClosedPositionValue,
177
+ isScreenReaderEnabled: false,
178
+ });
179
+
180
+ const result = computeContentOverlayBehavior(config, state);
181
+
182
+ expect(result.showDismiss).toBe(false);
183
+ });
184
+
185
+ it("returns false for default props with default state", () => {
186
+ const config = aConfig();
187
+ const state = aState();
188
+
189
+ const result = computeContentOverlayBehavior(config, state);
190
+
191
+ expect(result.showDismiss).toBe(false);
192
+ });
193
+ });
194
+
195
+ describe("combined behaviors", () => {
196
+ it("returns expected behavior for fullScreen overlay", () => {
197
+ const config = aConfig({
198
+ fullScreen: true,
199
+ isDraggable: false,
200
+ showDismiss: true,
201
+ });
202
+ const state = aState();
203
+
204
+ const result = computeContentOverlayBehavior(config, state);
205
+
206
+ expect(result).toEqual({
207
+ initialHeight: "fullScreen",
208
+ isDraggable: false,
209
+ showDismiss: true,
210
+ });
211
+ });
212
+
213
+ it("returns expected behavior for content-height overlay with adjustToContentHeight", () => {
214
+ const config = aConfig({
215
+ adjustToContentHeight: true,
216
+ showDismiss: true,
217
+ });
218
+ const state = aState();
219
+
220
+ const result = computeContentOverlayBehavior(config, state);
221
+
222
+ expect(result).toEqual({
223
+ initialHeight: "contentHeight",
224
+ isDraggable: true,
225
+ showDismiss: true,
226
+ });
227
+ });
228
+
229
+ it("returns expected behavior for overlay with onBeforeExit (confirmation flow)", () => {
230
+ const config = aConfig({
231
+ adjustToContentHeight: true,
232
+ hasOnBeforeExit: true,
233
+ isDraggable: true,
234
+ showDismiss: true,
235
+ });
236
+ const state = aState();
237
+
238
+ const result = computeContentOverlayBehavior(config, state);
239
+
240
+ expect(result).toEqual({
241
+ initialHeight: "contentHeight",
242
+ isDraggable: false,
243
+ showDismiss: true,
244
+ });
245
+ });
246
+
247
+ it("returns expected behavior for default props (legacy behavior)", () => {
248
+ const config = aConfig();
249
+ const state = aState();
250
+
251
+ const result = computeContentOverlayBehavior(config, state);
252
+
253
+ expect(result).toEqual({
254
+ initialHeight: "contentHeight",
255
+ isDraggable: true,
256
+ showDismiss: false,
257
+ });
258
+ });
259
+
260
+ it("returns expected behavior for accessibility case where screen reader forces dismiss button", () => {
261
+ const config = aConfig({
262
+ adjustToContentHeight: true,
263
+ showDismiss: false,
264
+ });
265
+ const state = aState({ isScreenReaderEnabled: true });
266
+
267
+ const result = computeContentOverlayBehavior(config, state);
268
+
269
+ expect(result).toEqual({
270
+ initialHeight: "contentHeight",
271
+ isDraggable: true,
272
+ showDismiss: true,
273
+ });
274
+ });
275
+ });
276
+ });
@@ -0,0 +1,119 @@
1
+ export interface ContentOverlayConfig {
2
+ fullScreen: boolean;
3
+ adjustToContentHeight: boolean;
4
+ isDraggable: boolean;
5
+ hasOnBeforeExit: boolean;
6
+ showDismiss: boolean;
7
+ }
8
+
9
+ export interface ContentOverlayState {
10
+ isScreenReaderEnabled: boolean;
11
+ // Pixel value of delta to the 100% position
12
+ position: number;
13
+ }
14
+
15
+ export type InitialHeight = "fullScreen" | "contentHeight";
16
+
17
+ export interface ContentOverlayBehavior {
18
+ initialHeight: InitialHeight;
19
+ isDraggable: boolean;
20
+ showDismiss: boolean;
21
+ }
22
+
23
+ /**
24
+ * Computes the abstract behavior of ContentOverlay from its props and state.
25
+ *
26
+ * This pure function documents and centralizes the complex logic that determines:
27
+ * - Initial height mode (fullScreen vs contentHeight)
28
+ * - Whether the overlay is draggable
29
+ * - Whether the dismiss button should be shown
30
+ *
31
+ * The logic accounts for legacy behavior where:
32
+ * - `onBeforeExit` silently overrides `isDraggable` to false
33
+ * - Default props (neither fullScreen nor adjustToContentHeight) are treated
34
+ * as contentHeight for the new implementation
35
+ * - Dismiss button visibility depends on multiple factors including position state
36
+ */
37
+ export function computeContentOverlayBehavior(
38
+ config: ContentOverlayConfig,
39
+ state: ContentOverlayState,
40
+ ): ContentOverlayBehavior {
41
+ const isDraggable = computeIsDraggable(config);
42
+ const initialHeight = computeInitialHeight(config, isDraggable);
43
+ const showDismiss = computeShowDismiss(config, state);
44
+
45
+ return {
46
+ initialHeight,
47
+ isDraggable,
48
+ showDismiss,
49
+ };
50
+ }
51
+
52
+ /**
53
+ * Order is important to maintain legacy behavior, despite the questionable logic.
54
+ * A non draggable overlay wants to be fullscreen, so as to have the dismiss button be visible.
55
+ * There is an invalid combination here with adjustToContentHeight and onBeforeExit which in turn overrides isDraggable to false.
56
+ * This requires an explicit showDismiss=true or else it will not be possible to dismiss the overlay.
57
+ */
58
+ function computeInitialHeight(
59
+ config: ContentOverlayConfig,
60
+ isDraggable: boolean,
61
+ ): InitialHeight {
62
+ if (config.adjustToContentHeight) {
63
+ return "contentHeight";
64
+ }
65
+
66
+ if (config.fullScreen) {
67
+ return "fullScreen";
68
+ }
69
+
70
+ if (!isDraggable) {
71
+ return "fullScreen";
72
+ }
73
+
74
+ return "contentHeight";
75
+ }
76
+
77
+ /**
78
+ * Draggability determination:
79
+ * - hasOnBeforeExit: true → false (silent override, regardless of isDraggable prop)
80
+ * - Otherwise → use isDraggable prop value
81
+ *
82
+ * This silent override exists because onBeforeExit needs to intercept close attempts,
83
+ * and dragging would bypass that interception.
84
+ */
85
+ function computeIsDraggable(config: ContentOverlayConfig): boolean {
86
+ if (config.hasOnBeforeExit) {
87
+ return false;
88
+ }
89
+
90
+ return config.isDraggable;
91
+ }
92
+
93
+ /**
94
+ * Dismiss button visibility:
95
+ * The idea behind fullscreen having it is that there may be little room to tap the background to dismiss.
96
+ * While this logic is redundant with the position, it's a relic of the legacy behavior where position didn't update in time.
97
+ */
98
+ function computeShowDismiss(
99
+ config: ContentOverlayConfig,
100
+ state: ContentOverlayState,
101
+ ): boolean {
102
+ if (config.showDismiss) {
103
+ return true;
104
+ }
105
+
106
+ if (state.isScreenReaderEnabled) {
107
+ return true;
108
+ }
109
+
110
+ if (config.fullScreen) {
111
+ return true;
112
+ }
113
+
114
+ if (!config.adjustToContentHeight && state.position === 0) {
115
+ return true;
116
+ }
117
+
118
+ return false;
119
+ }
@@ -0,0 +1 @@
1
+ export const KEYBOARD_TOP_PADDING_AUTO_SCROLL = 20;
@@ -0,0 +1,81 @@
1
+ import { act, renderHook } from "@testing-library/react-native";
2
+ import { BackHandler } from "react-native";
3
+ import { useBottomSheetModalBackHandler } from "./useBottomSheetModalBackHandler";
4
+
5
+ describe("useBottomSheetModalBackHandler", () => {
6
+ let mockRemove: jest.Mock;
7
+ let mockAddEventListener: jest.SpyInstance;
8
+
9
+ beforeEach(() => {
10
+ mockRemove = jest.fn();
11
+ mockAddEventListener = jest.spyOn(BackHandler, "addEventListener");
12
+ mockAddEventListener.mockReturnValue({ remove: mockRemove });
13
+ });
14
+
15
+ afterEach(() => {
16
+ mockAddEventListener.mockRestore();
17
+ });
18
+
19
+ it("should register BackHandler listener when sheet becomes visible", async () => {
20
+ const onCloseController = jest.fn();
21
+ const { result } = renderHook(() =>
22
+ useBottomSheetModalBackHandler(onCloseController),
23
+ );
24
+
25
+ await act(async () => {
26
+ result.current.handleSheetPositionChange(0);
27
+ });
28
+
29
+ expect(mockAddEventListener).toHaveBeenCalledWith(
30
+ "hardwareBackPress",
31
+ expect.any(Function),
32
+ );
33
+ });
34
+
35
+ it("should call onCloseController when back button is pressed", async () => {
36
+ const onCloseController = jest.fn();
37
+ const { result } = renderHook(() =>
38
+ useBottomSheetModalBackHandler(onCloseController),
39
+ );
40
+
41
+ await act(async () => {
42
+ result.current.handleSheetPositionChange(0);
43
+ });
44
+
45
+ const registeredCallback = mockAddEventListener.mock.calls[0][1];
46
+ const returnValue = registeredCallback();
47
+
48
+ expect(onCloseController).toHaveBeenCalled();
49
+ expect(returnValue).toBe(true);
50
+ });
51
+
52
+ it("should remove listener when sheet is dismissed", async () => {
53
+ const onCloseController = jest.fn();
54
+ const { result } = renderHook(() =>
55
+ useBottomSheetModalBackHandler(onCloseController),
56
+ );
57
+
58
+ await act(async () => {
59
+ result.current.handleSheetPositionChange(0);
60
+ });
61
+
62
+ await act(async () => {
63
+ result.current.handleSheetPositionChange(-1);
64
+ });
65
+
66
+ expect(mockRemove).toHaveBeenCalled();
67
+ });
68
+
69
+ it("should not register listener when index is negative", async () => {
70
+ const onCloseController = jest.fn();
71
+ const { result } = renderHook(() =>
72
+ useBottomSheetModalBackHandler(onCloseController),
73
+ );
74
+
75
+ await act(async () => {
76
+ result.current.handleSheetPositionChange(-1);
77
+ });
78
+
79
+ expect(mockAddEventListener).not.toHaveBeenCalled();
80
+ });
81
+ });
@@ -0,0 +1,36 @@
1
+ import { useCallback, useRef } from "react";
2
+ import { BackHandler, type NativeEventSubscription } from "react-native";
3
+
4
+ /**
5
+ * Hook that dismisses the bottom sheet on the hardware back button press if it is visible
6
+ * @param bottomSheetModalRef ref to the bottom sheet modal component
7
+ */
8
+ export function useBottomSheetModalBackHandler(onCloseController: () => void) {
9
+ const backHandlerSubscriptionRef = useRef<NativeEventSubscription | null>(
10
+ null,
11
+ );
12
+
13
+ const handleSheetPositionChange = useCallback(
14
+ (index: number) => {
15
+ const isBottomSheetModalVisible = index >= 0;
16
+
17
+ if (isBottomSheetModalVisible && !backHandlerSubscriptionRef.current) {
18
+ // Setup the back handler if the bottom sheet is right in front of the user
19
+ backHandlerSubscriptionRef.current = BackHandler.addEventListener(
20
+ "hardwareBackPress",
21
+ () => {
22
+ onCloseController();
23
+
24
+ return true;
25
+ },
26
+ );
27
+ } else if (!isBottomSheetModalVisible) {
28
+ backHandlerSubscriptionRef.current?.remove();
29
+ backHandlerSubscriptionRef.current = null;
30
+ }
31
+ },
32
+ [onCloseController],
33
+ );
34
+
35
+ return { handleSheetPositionChange };
36
+ }
@@ -1,2 +1,5 @@
1
- export { ContentOverlay } from "./ContentOverlay";
1
+ export {
2
+ ContentOverlay,
3
+ useIsKeyboardHandledByScrollView,
4
+ } from "./ContentOverlay";
2
5
  export type { ContentOverlayRef, ModalBackgroundColor } from "./types";
@@ -1,5 +1,4 @@
1
- import type { ReactNode } from "react";
2
- import type { Modalize } from "react-native-modalize";
1
+ import type { ReactNode, Ref } from "react";
3
2
 
4
3
  export interface ContentOverlayProps {
5
4
  /**
@@ -67,12 +66,6 @@ export interface ContentOverlayProps {
67
66
  */
68
67
  readonly onBeforeExit?: () => void;
69
68
 
70
- /**
71
- * Define the behavior of the keyboard when having inputs inside the modal.
72
- * @default padding
73
- */
74
- readonly keyboardAvoidingBehavior?: "height" | "padding" | "position";
75
-
76
69
  /**
77
70
  * Boolean to show a disabled state
78
71
  * @default false
@@ -80,17 +73,16 @@ export interface ContentOverlayProps {
80
73
  readonly loading?: boolean;
81
74
 
82
75
  /**
83
- * Define keyboard's Android behavior like iOS's one.
84
- * @default Platform.select({ ios: true, android: false })
76
+ * Ref to the content overlay component.
85
77
  */
86
- readonly avoidKeyboardLikeIOS?: boolean;
78
+ readonly ref?: Ref<ContentOverlayRef>;
87
79
  }
88
80
 
89
81
  export type ModalBackgroundColor = "surface" | "background";
90
82
 
91
83
  export type ContentOverlayRef =
92
84
  | {
93
- open?: Modalize["open"];
94
- close?: Modalize["close"];
85
+ open?: () => void;
86
+ close?: () => void;
95
87
  }
96
88
  | undefined;
@@ -836,4 +836,126 @@ describe("Transform", () => {
836
836
  });
837
837
  });
838
838
  });
839
+
840
+ describe("Bottom Sheet keyboard handling integration", () => {
841
+ beforeEach(() => {
842
+ jest.clearAllMocks();
843
+ });
844
+
845
+ // eslint-disable-next-line max-statements
846
+ it("updates animatedKeyboardState on focus when inside ContentOverlay", () => {
847
+ const mockSet = jest.fn();
848
+ const mockGet = jest.fn(() => ({ target: undefined }));
849
+ const mockKeyboardState = {
850
+ value: { target: undefined },
851
+ set: mockSet,
852
+ get: mockGet,
853
+ };
854
+
855
+ const mockTextInputNodesRef = { current: new Set<number>() };
856
+
857
+ // Mock the useBottomSheetInternal hook to simulate being inside ContentOverlay
858
+ jest
859
+ .spyOn(require("@gorhom/bottom-sheet"), "useBottomSheetInternal")
860
+ .mockReturnValue({
861
+ animatedKeyboardState: mockKeyboardState,
862
+ textInputNodesRef: mockTextInputNodesRef,
863
+ });
864
+
865
+ const a11yLabel = "Test InputText";
866
+ const { getByLabelText } = render(
867
+ <InputText accessibilityLabel={a11yLabel} />,
868
+ );
869
+
870
+ const input = getByLabelText(a11yLabel);
871
+
872
+ fireEvent(input, "onFocus", {
873
+ nativeEvent: { target: 123 },
874
+ });
875
+
876
+ expect(mockSet).toHaveBeenCalledWith(expect.any(Function));
877
+ // Verify the set callback updates the state correctly
878
+ const setCallback = mockSet.mock.calls[0][0];
879
+ const newState = setCallback({ target: undefined });
880
+ expect(newState).toEqual({ target: 123 });
881
+ });
882
+
883
+ // eslint-disable-next-line max-statements
884
+ it("consumer onFocus and onBlur callbacks still fire when inside ContentOverlay", () => {
885
+ const focusCallback = jest.fn();
886
+ const blurCallback = jest.fn();
887
+ const mockSet = jest.fn();
888
+ const mockGet = jest.fn(() => ({ target: undefined }));
889
+ const mockKeyboardState = {
890
+ value: { target: undefined },
891
+ set: mockSet,
892
+ get: mockGet,
893
+ };
894
+
895
+ const mockTextInputNodesRef = { current: new Set<number>() };
896
+
897
+ jest
898
+ .spyOn(require("@gorhom/bottom-sheet"), "useBottomSheetInternal")
899
+ .mockReturnValue({
900
+ animatedKeyboardState: mockKeyboardState,
901
+ textInputNodesRef: mockTextInputNodesRef,
902
+ });
903
+
904
+ const a11yLabel = "Test InputText";
905
+ const { getByLabelText } = render(
906
+ <InputText
907
+ onFocus={focusCallback}
908
+ onBlur={blurCallback}
909
+ accessibilityLabel={a11yLabel}
910
+ />,
911
+ );
912
+
913
+ const input = getByLabelText(a11yLabel);
914
+
915
+ fireEvent(input, "onFocus", {
916
+ nativeEvent: { target: 123 },
917
+ });
918
+ expect(focusCallback).toHaveBeenCalled();
919
+
920
+ fireEvent(input, "onBlur", {
921
+ nativeEvent: { target: 123 },
922
+ });
923
+ expect(blurCallback).toHaveBeenCalled();
924
+ });
925
+
926
+ it("handles value changes when inside ContentOverlay", () => {
927
+ const mockSet = jest.fn();
928
+ const mockGet = jest.fn(() => ({ target: undefined }));
929
+ const mockKeyboardState = {
930
+ value: { target: undefined },
931
+ set: mockSet,
932
+ get: mockGet,
933
+ };
934
+
935
+ const mockTextInputNodesRef = { current: new Set<number>() };
936
+ const changeCallback = jest.fn();
937
+
938
+ jest
939
+ .spyOn(require("@gorhom/bottom-sheet"), "useBottomSheetInternal")
940
+ .mockReturnValue({
941
+ animatedKeyboardState: mockKeyboardState,
942
+ textInputNodesRef: mockTextInputNodesRef,
943
+ });
944
+
945
+ const a11yLabel = "Test InputText";
946
+ const { getByLabelText } = render(
947
+ <InputText
948
+ onChangeText={changeCallback}
949
+ accessibilityLabel={a11yLabel}
950
+ />,
951
+ );
952
+
953
+ const input = getByLabelText(a11yLabel);
954
+
955
+ fireEvent.changeText(input, "New value");
956
+
957
+ // Value changes should work normally
958
+ expect(changeCallback).toHaveBeenCalledWith("New value");
959
+ });
960
+ });
839
961
  });