@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.
- package/dist/package.json +3 -6
- package/dist/src/AtlantisOverlayProvider/AtlantisOverlayProvider.js +5 -0
- package/dist/src/AtlantisOverlayProvider/index.js +1 -0
- package/dist/src/BottomSheet/BottomSheet.js +9 -11
- package/dist/src/BottomSheet/hooks/useBottomSheetBackHandler.js +2 -2
- package/dist/src/ButtonGroup/components/SecondaryActionSheet/SecondaryActionSheet.js +9 -11
- package/dist/src/ContentOverlay/BottomSheetKeyboardAwareScrollView.js +19 -0
- package/dist/src/ContentOverlay/ContentOverlay.js +143 -107
- package/dist/src/ContentOverlay/ContentOverlay.style.js +8 -12
- package/dist/src/ContentOverlay/computeContentOverlayBehavior.js +76 -0
- package/dist/src/ContentOverlay/constants.js +1 -0
- package/dist/src/ContentOverlay/hooks/useBottomSheetModalBackHandler.js +25 -0
- package/dist/src/ContentOverlay/index.js +1 -1
- package/dist/src/FormatFile/components/FormatFileBottomSheet/FormatFileBottomSheet.js +7 -9
- package/dist/src/InputText/InputText.js +44 -1
- package/dist/src/index.js +1 -0
- package/dist/src/utils/meta/meta.json +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/dist/types/src/AtlantisOverlayProvider/AtlantisOverlayProvider.d.ts +6 -0
- package/dist/types/src/AtlantisOverlayProvider/index.d.ts +1 -0
- package/dist/types/src/BottomSheet/hooks/useBottomSheetBackHandler.d.ts +3 -3
- package/dist/types/src/ContentOverlay/BottomSheetKeyboardAwareScrollView.d.ts +11 -0
- package/dist/types/src/ContentOverlay/ContentOverlay.d.ts +2 -5
- package/dist/types/src/ContentOverlay/ContentOverlay.style.d.ts +11 -10
- package/dist/types/src/ContentOverlay/computeContentOverlayBehavior.d.ts +32 -0
- package/dist/types/src/ContentOverlay/constants.d.ts +1 -0
- package/dist/types/src/ContentOverlay/hooks/useBottomSheetModalBackHandler.d.ts +7 -0
- package/dist/types/src/ContentOverlay/index.d.ts +1 -1
- package/dist/types/src/ContentOverlay/types.d.ts +5 -12
- package/dist/types/src/index.d.ts +1 -0
- package/jestSetup.js +2 -0
- package/package.json +3 -6
- package/src/AtlantisOverlayProvider/AtlantisOverlayProvider.tsx +12 -0
- package/src/AtlantisOverlayProvider/index.ts +1 -0
- package/src/BottomSheet/BottomSheet.tsx +13 -13
- package/src/BottomSheet/hooks/useBottomSheetBackHandler.test.ts +10 -10
- package/src/BottomSheet/hooks/useBottomSheetBackHandler.ts +4 -4
- package/src/ButtonGroup/ButtonGroup.stories.tsx +10 -8
- package/src/ButtonGroup/ButtonGroup.test.tsx +7 -10
- package/src/ButtonGroup/components/SecondaryActionSheet/SecondaryActionSheet.tsx +26 -29
- package/src/ContentOverlay/BottomSheetKeyboardAwareScrollView.tsx +36 -0
- package/src/ContentOverlay/ContentOverlay.stories.tsx +32 -36
- package/src/ContentOverlay/ContentOverlay.style.ts +12 -12
- package/src/ContentOverlay/ContentOverlay.test.tsx +157 -79
- package/src/ContentOverlay/ContentOverlay.tsx +247 -205
- package/src/ContentOverlay/computeContentOverlayBehavior.test.ts +276 -0
- package/src/ContentOverlay/computeContentOverlayBehavior.ts +119 -0
- package/src/ContentOverlay/constants.ts +1 -0
- package/src/ContentOverlay/hooks/useBottomSheetModalBackHandler.test.ts +81 -0
- package/src/ContentOverlay/hooks/useBottomSheetModalBackHandler.ts +36 -0
- package/src/ContentOverlay/index.ts +4 -1
- package/src/ContentOverlay/types.ts +5 -13
- package/src/Form/Form.stories.tsx +8 -4
- package/src/Form/Form.test.tsx +51 -54
- package/src/Form/components/FormSaveButton/FormSaveButton.test.tsx +7 -10
- package/src/FormatFile/FormatFile.stories.tsx +3 -4
- package/src/FormatFile/FormatFile.test.tsx +11 -14
- package/src/FormatFile/components/FormatFileBottomSheet/FormatFileBottomSheet.test.tsx +6 -9
- package/src/FormatFile/components/FormatFileBottomSheet/FormatFileBottomSheet.tsx +21 -24
- package/src/InputDate/InputDate.test.tsx +5 -8
- package/src/InputText/InputText.test.tsx +122 -0
- package/src/InputText/InputText.tsx +62 -2
- package/src/InputTime/InputTime.stories.tsx +8 -4
- package/src/InputTime/InputTime.test.tsx +5 -8
- package/src/ThumbnailList/ThumbnailList.stories.tsx +6 -4
- package/src/ThumbnailList/ThumbnailList.test.tsx +5 -8
- package/src/ThumbnailList/__snapshots__/ThumbnailList.test.tsx.snap +101 -150
- package/src/index.ts +1 -0
- package/src/utils/meta/meta.json +2 -1
- package/dist/src/ContentOverlay/UNSAFE_WrappedModalize.js +0 -23
- package/dist/types/src/ContentOverlay/UNSAFE_WrappedModalize.d.ts +0 -3
- package/src/ContentOverlay/UNSAFE_WrappedModalize.tsx +0 -41
package/src/Form/Form.test.tsx
CHANGED
|
@@ -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
|
-
<
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
UNSAFE_allowDiscardLocalCacheWhenOffline
|
|
160
|
-
|
|
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={
|
|
165
|
-
placeholder={
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
<
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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
|
-
<
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
<
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
<
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
<
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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<
|
|
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 {
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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
|
-
<
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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 {
|
|
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
|
-
<
|
|
22
|
+
<AtlantisOverlayProvider>
|
|
21
23
|
<ThumbnailList {...args} />
|
|
22
|
-
</
|
|
24
|
+
</AtlantisOverlayProvider>
|
|
23
25
|
</SafeAreaProvider>
|
|
24
26
|
);
|
|
25
27
|
|