@jobber/components-native 0.98.5 → 0.100.0

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 (72) hide show
  1. package/dist/package.json +3 -6
  2. package/dist/src/AtlantisOverlayProvider/AtlantisOverlayProvider.js +5 -0
  3. package/dist/src/AtlantisOverlayProvider/index.js +1 -0
  4. package/dist/src/BottomSheet/BottomSheet.js +9 -11
  5. package/dist/src/BottomSheet/hooks/useBottomSheetBackHandler.js +2 -2
  6. package/dist/src/ButtonGroup/components/SecondaryActionSheet/SecondaryActionSheet.js +9 -11
  7. package/dist/src/ContentOverlay/BottomSheetKeyboardAwareScrollView.js +19 -0
  8. package/dist/src/ContentOverlay/ContentOverlay.js +143 -107
  9. package/dist/src/ContentOverlay/ContentOverlay.style.js +8 -12
  10. package/dist/src/ContentOverlay/computeContentOverlayBehavior.js +76 -0
  11. package/dist/src/ContentOverlay/constants.js +1 -0
  12. package/dist/src/ContentOverlay/hooks/useBottomSheetModalBackHandler.js +25 -0
  13. package/dist/src/ContentOverlay/index.js +1 -1
  14. package/dist/src/FormatFile/components/FormatFileBottomSheet/FormatFileBottomSheet.js +7 -9
  15. package/dist/src/InputText/InputText.js +44 -1
  16. package/dist/src/index.js +1 -0
  17. package/dist/src/utils/meta/meta.json +1 -0
  18. package/dist/tsconfig.build.tsbuildinfo +1 -1
  19. package/dist/types/src/AtlantisOverlayProvider/AtlantisOverlayProvider.d.ts +6 -0
  20. package/dist/types/src/AtlantisOverlayProvider/index.d.ts +1 -0
  21. package/dist/types/src/BottomSheet/hooks/useBottomSheetBackHandler.d.ts +3 -3
  22. package/dist/types/src/ContentOverlay/BottomSheetKeyboardAwareScrollView.d.ts +11 -0
  23. package/dist/types/src/ContentOverlay/ContentOverlay.d.ts +2 -5
  24. package/dist/types/src/ContentOverlay/ContentOverlay.style.d.ts +11 -10
  25. package/dist/types/src/ContentOverlay/computeContentOverlayBehavior.d.ts +32 -0
  26. package/dist/types/src/ContentOverlay/constants.d.ts +1 -0
  27. package/dist/types/src/ContentOverlay/hooks/useBottomSheetModalBackHandler.d.ts +7 -0
  28. package/dist/types/src/ContentOverlay/index.d.ts +1 -1
  29. package/dist/types/src/ContentOverlay/types.d.ts +5 -12
  30. package/dist/types/src/index.d.ts +1 -0
  31. package/jestSetup.js +2 -0
  32. package/package.json +3 -6
  33. package/src/AtlantisOverlayProvider/AtlantisOverlayProvider.tsx +12 -0
  34. package/src/AtlantisOverlayProvider/index.ts +1 -0
  35. package/src/BottomSheet/BottomSheet.tsx +13 -13
  36. package/src/BottomSheet/hooks/useBottomSheetBackHandler.test.ts +10 -10
  37. package/src/BottomSheet/hooks/useBottomSheetBackHandler.ts +4 -4
  38. package/src/ButtonGroup/ButtonGroup.stories.tsx +10 -8
  39. package/src/ButtonGroup/ButtonGroup.test.tsx +7 -10
  40. package/src/ButtonGroup/components/SecondaryActionSheet/SecondaryActionSheet.tsx +26 -29
  41. package/src/ContentOverlay/BottomSheetKeyboardAwareScrollView.tsx +36 -0
  42. package/src/ContentOverlay/ContentOverlay.stories.tsx +32 -36
  43. package/src/ContentOverlay/ContentOverlay.style.ts +12 -12
  44. package/src/ContentOverlay/ContentOverlay.test.tsx +157 -79
  45. package/src/ContentOverlay/ContentOverlay.tsx +247 -205
  46. package/src/ContentOverlay/computeContentOverlayBehavior.test.ts +276 -0
  47. package/src/ContentOverlay/computeContentOverlayBehavior.ts +119 -0
  48. package/src/ContentOverlay/constants.ts +1 -0
  49. package/src/ContentOverlay/hooks/useBottomSheetModalBackHandler.test.ts +81 -0
  50. package/src/ContentOverlay/hooks/useBottomSheetModalBackHandler.ts +36 -0
  51. package/src/ContentOverlay/index.ts +4 -1
  52. package/src/ContentOverlay/types.ts +5 -13
  53. package/src/Form/Form.stories.tsx +8 -4
  54. package/src/Form/Form.test.tsx +51 -54
  55. package/src/Form/components/FormSaveButton/FormSaveButton.test.tsx +7 -10
  56. package/src/FormatFile/FormatFile.stories.tsx +3 -4
  57. package/src/FormatFile/FormatFile.test.tsx +11 -14
  58. package/src/FormatFile/components/FormatFileBottomSheet/FormatFileBottomSheet.test.tsx +6 -9
  59. package/src/FormatFile/components/FormatFileBottomSheet/FormatFileBottomSheet.tsx +21 -24
  60. package/src/InputDate/InputDate.test.tsx +5 -8
  61. package/src/InputText/InputText.test.tsx +122 -0
  62. package/src/InputText/InputText.tsx +62 -2
  63. package/src/InputTime/InputTime.stories.tsx +8 -4
  64. package/src/InputTime/InputTime.test.tsx +5 -8
  65. package/src/ThumbnailList/ThumbnailList.stories.tsx +6 -4
  66. package/src/ThumbnailList/ThumbnailList.test.tsx +5 -8
  67. package/src/ThumbnailList/__snapshots__/ThumbnailList.test.tsx.snap +101 -150
  68. package/src/index.ts +1 -0
  69. package/src/utils/meta/meta.json +2 -1
  70. package/dist/src/ContentOverlay/UNSAFE_WrappedModalize.js +0 -23
  71. package/dist/types/src/ContentOverlay/UNSAFE_WrappedModalize.d.ts +0 -3
  72. package/src/ContentOverlay/UNSAFE_WrappedModalize.tsx +0 -41
@@ -1,7 +1,6 @@
1
1
  import React, { type ReactElement } from "react";
2
2
  import { act, fireEvent, render, waitFor } from "@testing-library/react-native";
3
3
  import { Alert, Keyboard } from "react-native";
4
- import { Host } from "react-native-portalize";
5
4
  import type { FormBannerMessage } from ".";
6
5
  import { Form, FormBannerMessageType } from ".";
7
6
  import type { FormBannerErrors } from "./types";
@@ -139,61 +138,59 @@ function MockForm({
139
138
  }
140
139
 
141
140
  return (
142
- <Host>
143
- <Form
144
- onSubmit={onSubmit}
145
- onSubmitError={onErrorMock}
146
- onSubmitSuccess={onSuccessMock}
147
- bannerErrors={formErrors}
148
- bannerMessages={bannerMessages}
149
- saveButtonLabel={saveLabel}
150
- renderStickySection={renderStickySection}
151
- initialLoading={initialLoading}
152
- initialValues={initialValues}
153
- localCacheKey={localCacheKey}
154
- localCacheExclude={localCacheExclude}
155
- localCacheId={localCacheId}
156
- onBeforeSubmit={onBeforeSubmit}
157
- renderFooter={renderFooter}
158
- saveButtonOffset={saveButtonOffset}
159
- UNSAFE_allowDiscardLocalCacheWhenOffline={
160
- UNSAFE_allowDiscardLocalCacheWhenOffline
161
- }
162
- >
141
+ <Form
142
+ onSubmit={onSubmit}
143
+ onSubmitError={onErrorMock}
144
+ onSubmitSuccess={onSuccessMock}
145
+ bannerErrors={formErrors}
146
+ bannerMessages={bannerMessages}
147
+ saveButtonLabel={saveLabel}
148
+ renderStickySection={renderStickySection}
149
+ initialLoading={initialLoading}
150
+ initialValues={initialValues}
151
+ localCacheKey={localCacheKey}
152
+ localCacheExclude={localCacheExclude}
153
+ localCacheId={localCacheId}
154
+ onBeforeSubmit={onBeforeSubmit}
155
+ renderFooter={renderFooter}
156
+ saveButtonOffset={saveButtonOffset}
157
+ UNSAFE_allowDiscardLocalCacheWhenOffline={
158
+ UNSAFE_allowDiscardLocalCacheWhenOffline
159
+ }
160
+ >
161
+ <InputText
162
+ name={testInputTextName}
163
+ placeholder={testInputTextPlaceholder}
164
+ onChangeText={onChangeMock}
165
+ accessibilityLabel={testInputTextPlaceholder}
166
+ validations={{
167
+ required: requiredInputText,
168
+ minLength: { value: 3, message: minLengthText },
169
+ }}
170
+ />
171
+ {Array.isArray(localCacheExclude) && localCacheExclude.length > 0 && (
163
172
  <InputText
164
- name={testInputTextName}
165
- placeholder={testInputTextPlaceholder}
166
- onChangeText={onChangeMock}
167
- accessibilityLabel={testInputTextPlaceholder}
168
- validations={{
169
- required: requiredInputText,
170
- minLength: { value: 3, message: minLengthText },
171
- }}
172
- />
173
- {Array.isArray(localCacheExclude) && localCacheExclude.length > 0 && (
174
- <InputText
175
- name={testInputTextNameExclude}
176
- placeholder={testInputTextPlaceholderExclude}
177
- />
178
- )}
179
- <Select
180
- onChange={onChangeSelectMock}
181
- label={selectLabel}
182
- name={testSelectName}
183
- >
184
- <Option value={"1"}>1</Option>
185
- <Option value={"2"}>2</Option>
186
- </Select>
187
- <Switch
188
- name={testSwitchName}
189
- label="Test Switch"
190
- accessibilityLabel={switchLabel}
191
- onValueChange={onChangeSwitchMock}
173
+ name={testInputTextNameExclude}
174
+ placeholder={testInputTextPlaceholderExclude}
192
175
  />
193
- <InputNumber name={testInputNumberName} placeholder="Test Num" />
194
- <Checkbox name={testCheckboxName} accessibilityLabel={checkboxLabel} />
195
- </Form>
196
- </Host>
176
+ )}
177
+ <Select
178
+ onChange={onChangeSelectMock}
179
+ label={selectLabel}
180
+ name={testSelectName}
181
+ >
182
+ <Option value={"1"}>1</Option>
183
+ <Option value={"2"}>2</Option>
184
+ </Select>
185
+ <Switch
186
+ name={testSwitchName}
187
+ label="Test Switch"
188
+ accessibilityLabel={switchLabel}
189
+ onValueChange={onChangeSwitchMock}
190
+ />
191
+ <InputNumber name={testInputNumberName} placeholder="Test Num" />
192
+ <Checkbox name={testCheckboxName} accessibilityLabel={checkboxLabel} />
193
+ </Form>
197
194
  );
198
195
  }
199
196
 
@@ -1,6 +1,5 @@
1
1
  import React from "react";
2
2
  import { fireEvent, render, waitFor } from "@testing-library/react-native";
3
- import { Host } from "react-native-portalize";
4
3
  import type { IconNames } from "@jobber/design";
5
4
  import { FormSaveButton } from "./FormSaveButton";
6
5
 
@@ -31,15 +30,13 @@ jest.mock("react-hook-form", () => ({
31
30
 
32
31
  function ButtonGroupForTest(props: TestFormSaveButtonProps) {
33
32
  return (
34
- <Host>
35
- <FormSaveButton
36
- primaryAction={props.primaryAction}
37
- loading={props.loading}
38
- label={props.label}
39
- setSecondaryActionLoading={props.setSecondaryActionLoading}
40
- secondaryActions={props.secondaryAction}
41
- />
42
- </Host>
33
+ <FormSaveButton
34
+ primaryAction={props.primaryAction}
35
+ loading={props.loading}
36
+ label={props.label}
37
+ setSecondaryActionLoading={props.setSecondaryActionLoading}
38
+ secondaryActions={props.secondaryAction}
39
+ />
43
40
  );
44
41
  }
45
42
 
@@ -1,8 +1,7 @@
1
1
  import React from "react";
2
2
  import type { Meta, StoryObj } from "@storybook/react-native-web-vite";
3
- import { Host } from "react-native-portalize";
4
3
  import { SafeAreaProvider } from "react-native-safe-area-context";
5
- import { FormatFile } from "@jobber/components-native";
4
+ import { AtlantisOverlayProvider, FormatFile } from "@jobber/components-native";
6
5
 
7
6
  const meta = {
8
7
  title: "Components/Images and Icons/FormatFile",
@@ -17,7 +16,7 @@ type Story = StoryObj<Partial<React.ComponentProps<typeof FormatFile>>>;
17
16
 
18
17
  const BasicTemplate = (args: Story["args"]) => (
19
18
  <SafeAreaProvider>
20
- <Host>
19
+ <AtlantisOverlayProvider>
21
20
  <FormatFile
22
21
  file={
23
22
  args?.file ?? {
@@ -39,7 +38,7 @@ const BasicTemplate = (args: Story["args"]) => (
39
38
  showFileTypeIndicator={args?.showFileTypeIndicator}
40
39
  createThumbnail={args?.createThumbnail}
41
40
  />
42
- </Host>
41
+ </AtlantisOverlayProvider>
43
42
  </SafeAreaProvider>
44
43
  );
45
44
 
@@ -2,7 +2,6 @@ import React from "react";
2
2
  import type { RenderAPI } from "@testing-library/react-native";
3
3
  import { fireEvent, render, waitFor } from "@testing-library/react-native";
4
4
  import { Alert } from "react-native";
5
- import { Host } from "react-native-portalize";
6
5
  import type { File } from ".";
7
6
  import { FormatFile } from ".";
8
7
  import {
@@ -42,19 +41,17 @@ const renderFormatFile = (
42
41
  showFileTypeIndicator?: boolean,
43
42
  ) => {
44
43
  return render(
45
- <Host>
46
- <FormatFile
47
- file={file}
48
- accessibilityLabel="Custom Label"
49
- accessibilityHint="Custom Hint Text"
50
- onTap={() => Alert.alert("alert")}
51
- onRemove={onRemove}
52
- bottomSheetOptionsSuffix={bottomSheetOptionsSuffix}
53
- showFileTypeIndicator={showFileTypeIndicator}
54
- onPreviewPress={mockOnPreview}
55
- createThumbnail={mockCreateThumbnail}
56
- />
57
- </Host>,
44
+ <FormatFile
45
+ file={file}
46
+ accessibilityLabel="Custom Label"
47
+ accessibilityHint="Custom Hint Text"
48
+ onTap={() => Alert.alert("alert")}
49
+ onRemove={onRemove}
50
+ bottomSheetOptionsSuffix={bottomSheetOptionsSuffix}
51
+ showFileTypeIndicator={showFileTypeIndicator}
52
+ onPreviewPress={mockOnPreview}
53
+ createThumbnail={mockCreateThumbnail}
54
+ />,
58
55
  );
59
56
  };
60
57
 
@@ -1,7 +1,6 @@
1
1
  import React, { createRef } from "react";
2
2
  import type { RenderAPI } from "@testing-library/react-native";
3
3
  import { fireEvent, render } from "@testing-library/react-native";
4
- import { Host } from "react-native-portalize";
5
4
  import { act } from "react-test-renderer";
6
5
  import type { BottomSheetOptionsSuffix } from "./FormatFileBottomSheet";
7
6
  import { FormatFileBottomSheet } from "./FormatFileBottomSheet";
@@ -20,14 +19,12 @@ const renderBottomSheet = (
20
19
  bottomSheetOptionsSuffix: BottomSheetOptionsSuffix,
21
20
  ) => {
22
21
  return render(
23
- <Host>
24
- <FormatFileBottomSheet
25
- onPreviewPress={onPreview}
26
- onRemovePress={onRemove}
27
- bottomSheetRef={bottomSheetRef}
28
- bottomSheetOptionsSuffix={bottomSheetOptionsSuffix}
29
- />
30
- </Host>,
22
+ <FormatFileBottomSheet
23
+ onPreviewPress={onPreview}
24
+ onRemovePress={onRemove}
25
+ bottomSheetRef={bottomSheetRef}
26
+ bottomSheetOptionsSuffix={bottomSheetOptionsSuffix}
27
+ />,
31
28
  );
32
29
  };
33
30
 
@@ -1,6 +1,5 @@
1
1
  import type { RefObject } from "react";
2
2
  import React from "react";
3
- import { Portal } from "react-native-portalize";
4
3
  import type { BottomSheetRef } from "../../../BottomSheet/BottomSheet";
5
4
  import { BottomSheet } from "../../../BottomSheet/BottomSheet";
6
5
  import { BottomSheetOption } from "../../../BottomSheet/components/BottomSheetOption";
@@ -30,28 +29,26 @@ export const FormatFileBottomSheet = ({
30
29
  };
31
30
 
32
31
  return (
33
- <Portal>
34
- <BottomSheet ref={bottomSheetRef}>
35
- {onPreviewPress ? (
36
- <BottomSheetOption
37
- icon={"eye"}
38
- text={t("FormatFile.preview", {
39
- item: bottomSheetOptionsSuffix || "",
40
- })}
41
- onPress={() => handlePress(onPreviewPress)}
42
- />
43
- ) : undefined}
44
- {onRemovePress ? (
45
- <BottomSheetOption
46
- icon={"trash"}
47
- destructive={true}
48
- text={t("FormatFile.remove", {
49
- item: bottomSheetOptionsSuffix || "",
50
- })}
51
- onPress={() => handlePress(onRemovePress)}
52
- />
53
- ) : undefined}
54
- </BottomSheet>
55
- </Portal>
32
+ <BottomSheet ref={bottomSheetRef}>
33
+ {onPreviewPress ? (
34
+ <BottomSheetOption
35
+ icon={"eye"}
36
+ text={t("FormatFile.preview", {
37
+ item: bottomSheetOptionsSuffix || "",
38
+ })}
39
+ onPress={() => handlePress(onPreviewPress)}
40
+ />
41
+ ) : undefined}
42
+ {onRemovePress ? (
43
+ <BottomSheetOption
44
+ icon={"trash"}
45
+ destructive={true}
46
+ text={t("FormatFile.remove", {
47
+ item: bottomSheetOptionsSuffix || "",
48
+ })}
49
+ onPress={() => handlePress(onRemovePress)}
50
+ />
51
+ ) : undefined}
52
+ </BottomSheet>
56
53
  );
57
54
  };
@@ -1,6 +1,5 @@
1
1
  import React from "react";
2
2
  import { fireEvent, render, waitFor } from "@testing-library/react-native";
3
- import { Host } from "react-native-portalize";
4
3
  import { FormProvider, useForm } from "react-hook-form";
5
4
  import { Keyboard } from "react-native";
6
5
  import { InputDate } from "./InputDate";
@@ -175,13 +174,11 @@ describe("InputDate", () => {
175
174
  const setup = () =>
176
175
  render(
177
176
  <SimpleFormWithProvider defaultValues={{ [pickerName]: value }}>
178
- <Host>
179
- <InputDate
180
- name={pickerName}
181
- onChange={handleChange}
182
- validations={{ required: requiredError }}
183
- />
184
- </Host>
177
+ <InputDate
178
+ name={pickerName}
179
+ onChange={handleChange}
180
+ validations={{ required: requiredError }}
181
+ />
185
182
  </SimpleFormWithProvider>,
186
183
  );
187
184
 
@@ -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
  });
@@ -14,7 +14,8 @@ import type {
14
14
  TextInputProps,
15
15
  TextStyle,
16
16
  } from "react-native";
17
- import { Platform, TextInput } from "react-native";
17
+ import { Platform, TextInput, findNodeHandle } from "react-native";
18
+ import { useBottomSheetInternal } from "@gorhom/bottom-sheet";
18
19
  import type { RegisterOptions } from "react-hook-form";
19
20
  import type { IconNames } from "@jobber/design";
20
21
  import identity from "lodash/identity";
@@ -22,6 +23,7 @@ import type { Clearable } from "@jobber/hooks";
22
23
  import { useShowClear } from "@jobber/hooks";
23
24
  import { useStyles } from "./InputText.style";
24
25
  import { useInputAccessoriesContext } from "./context";
26
+ import { useIsKeyboardHandledByScrollView } from "../ContentOverlay";
25
27
  import { useFormController } from "../hooks";
26
28
  import type {
27
29
  InputFieldStyleOverride,
@@ -315,6 +317,20 @@ function InputTextInternal(
315
317
  disabled,
316
318
  });
317
319
 
320
+ // When inside a scrollable ContentOverlay, keyboard offset is handled by
321
+ // KeyboardAwareScrollView. Registering with the bottom-sheet's keyboard
322
+ // state would cause double-counted spacing, so we skip it.
323
+ const isKeyboardHandledByScrollView = useIsKeyboardHandledByScrollView();
324
+ const bottomSheetContext = useBottomSheetInternal(true);
325
+ const shouldHandleBottomSheetKeyboard =
326
+ bottomSheetContext !== null && !isKeyboardHandledByScrollView;
327
+ const animatedKeyboardState = shouldHandleBottomSheetKeyboard
328
+ ? bottomSheetContext.animatedKeyboardState
329
+ : undefined;
330
+ const textInputNodesRef = shouldHandleBottomSheetKeyboard
331
+ ? bottomSheetContext.textInputNodesRef
332
+ : undefined;
333
+
318
334
  // Android doesn't have an accessibility label like iOS does. By adding
319
335
  // it as a placeholder it readds it like a label. However we don't want to
320
336
  // add a placeholder on iOS.
@@ -439,11 +455,13 @@ function InputTextInternal(
439
455
  secureTextEntry={secureTextEntry}
440
456
  {...androidA11yProps}
441
457
  onFocus={event => {
458
+ handleBottomSheetFocus(event);
442
459
  _name && setFocusedInput(_name);
443
460
  setFocused(true);
444
461
  onFocus?.(event);
445
462
  }}
446
463
  onBlur={event => {
464
+ handleBottomSheetBlur(event);
447
465
  _name && setFocusedInput("");
448
466
  setFocused(false);
449
467
  onBlur?.(event);
@@ -470,6 +488,48 @@ function InputTextInternal(
470
488
  updateFormAndState(removedIOSCharValue);
471
489
  }
472
490
 
491
+ function handleBottomSheetFocus(event?: FocusEvent) {
492
+ if (!animatedKeyboardState || !textInputNodesRef || !event?.nativeEvent) {
493
+ return;
494
+ }
495
+
496
+ animatedKeyboardState.set(state => ({
497
+ ...state,
498
+ target: event.nativeEvent.target,
499
+ }));
500
+ }
501
+
502
+ function handleBottomSheetBlur(event?: FocusEvent) {
503
+ if (!animatedKeyboardState || !textInputNodesRef || !event?.nativeEvent) {
504
+ return;
505
+ }
506
+ const keyboardState = animatedKeyboardState.get();
507
+ const currentlyFocusedInput = TextInput.State.currentlyFocusedInput();
508
+ const currentFocusedInput =
509
+ currentlyFocusedInput !== null
510
+ ? findNodeHandle(
511
+ // @ts-expect-error - TextInput.State.currentlyFocusedInput() returns NativeMethods
512
+ // which is not directly assignable to findNodeHandle's expected type,
513
+ // but it works at runtime. This is a known type limitation in React Native.
514
+ currentlyFocusedInput,
515
+ )
516
+ : null;
517
+
518
+ // Only remove the target if it belongs to the current component
519
+ // and if the currently focused input is not in the targets set
520
+ const shouldRemoveCurrentTarget =
521
+ keyboardState.target === event.nativeEvent.target;
522
+ const shouldIgnoreBlurEvent =
523
+ currentFocusedInput && textInputNodesRef.current.has(currentFocusedInput);
524
+
525
+ if (shouldRemoveCurrentTarget && !shouldIgnoreBlurEvent) {
526
+ animatedKeyboardState.set(state => ({
527
+ ...state,
528
+ target: undefined,
529
+ }));
530
+ }
531
+ }
532
+
473
533
  function handleClear() {
474
534
  handleChangeText("");
475
535
  }
@@ -516,7 +576,7 @@ interface UseTextInputRefProps {
516
576
  }
517
577
 
518
578
  function useTextInputRef({ ref, onClear }: UseTextInputRefProps) {
519
- const textInputRef = useRef<InputTextRef | null>(null);
579
+ const textInputRef = useRef<TextInput | null>(null);
520
580
 
521
581
  useImperativeHandle(
522
582
  ref,
@@ -1,8 +1,12 @@
1
1
  import React, { useState } from "react";
2
2
  import type { Meta, StoryObj } from "@storybook/react-native-web-vite";
3
- import { Host } from "react-native-portalize";
4
3
  import { SafeAreaProvider } from "react-native-safe-area-context";
5
- import { Content, Form, InputTime } from "@jobber/components-native";
4
+ import {
5
+ AtlantisOverlayProvider,
6
+ Content,
7
+ Form,
8
+ InputTime,
9
+ } from "@jobber/components-native";
6
10
 
7
11
  const meta = {
8
12
  title: "Components/Forms and Inputs/InputTime",
@@ -63,7 +67,7 @@ const EmptyValueTemplate = (args: ControlledStory["args"]) => {
63
67
 
64
68
  const FormControlledTemplate = (args: FormStory["args"]) => (
65
69
  <SafeAreaProvider>
66
- <Host>
70
+ <AtlantisOverlayProvider>
67
71
  <Form
68
72
  initialValues={{ startTime: new Date("2023-07-21T16:36:34.873Z") }}
69
73
  onSubmit={value =>
@@ -84,7 +88,7 @@ const FormControlledTemplate = (args: FormStory["args"]) => (
84
88
  />
85
89
  </Content>
86
90
  </Form>
87
- </Host>
91
+ </AtlantisOverlayProvider>
88
92
  </SafeAreaProvider>
89
93
  );
90
94
 
@@ -1,6 +1,5 @@
1
1
  import React from "react";
2
2
  import { fireEvent, render, waitFor } from "@testing-library/react-native";
3
- import { Host } from "react-native-portalize";
4
3
  import { FormProvider, useForm } from "react-hook-form";
5
4
  import { Keyboard } from "react-native";
6
5
  import { InputTime } from "./InputTime";
@@ -223,13 +222,11 @@ describe("Form controlled", () => {
223
222
  const setup = () =>
224
223
  render(
225
224
  <SimpleFormWithProvider defaultValues={{ [pickerName]: value }}>
226
- <Host>
227
- <InputTime
228
- name={pickerName}
229
- onChange={handleChange}
230
- validations={{ required: requiredError }}
231
- />
232
- </Host>
225
+ <InputTime
226
+ name={pickerName}
227
+ onChange={handleChange}
228
+ validations={{ required: requiredError }}
229
+ />
233
230
  </SimpleFormWithProvider>,
234
231
  );
235
232
 
@@ -1,8 +1,10 @@
1
1
  import React from "react";
2
2
  import type { Meta, StoryObj } from "@storybook/react-native-web-vite";
3
- import { Host } from "react-native-portalize";
4
3
  import { SafeAreaProvider } from "react-native-safe-area-context";
5
- import { ThumbnailList } from "@jobber/components-native";
4
+ import {
5
+ AtlantisOverlayProvider,
6
+ ThumbnailList,
7
+ } from "@jobber/components-native";
6
8
 
7
9
  const meta = {
8
10
  title: "Components/Images and Icons/ThumbnailList",
@@ -17,9 +19,9 @@ type Story = StoryObj<typeof meta>;
17
19
 
18
20
  const BasicTemplate = (args: Story["args"]) => (
19
21
  <SafeAreaProvider>
20
- <Host>
22
+ <AtlantisOverlayProvider>
21
23
  <ThumbnailList {...args} />
22
- </Host>
24
+ </AtlantisOverlayProvider>
23
25
  </SafeAreaProvider>
24
26
  );
25
27