@jobber/components-native 0.38.0 → 0.40.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/src/AtlantisContext/AtlantisContext.js +2 -0
- package/dist/src/Form/Form.js +187 -0
- package/dist/src/Form/Form.style.js +33 -0
- package/dist/src/Form/components/FormActionBar/FormActionBar.js +21 -0
- package/dist/src/Form/components/FormActionBar/FormActionBar.style.js +5 -0
- package/dist/src/Form/components/FormActionBar/index.js +1 -0
- package/dist/src/Form/components/FormBody/FormBody.js +20 -0
- package/dist/src/Form/components/FormBody/FormBody.style.js +26 -0
- package/dist/src/Form/components/FormBody/index.js +1 -0
- package/dist/src/Form/components/FormCache/FormCache.js +34 -0
- package/dist/src/Form/components/FormErrorBanner/FormErrorBanner.js +21 -0
- package/dist/src/Form/components/FormErrorBanner/index.js +1 -0
- package/dist/src/Form/components/FormErrorBanner/messages.js +13 -0
- package/dist/src/Form/components/FormMask/FormMask.js +11 -0
- package/dist/src/Form/components/FormMask/FormMask.style.js +15 -0
- package/dist/src/Form/components/FormMask/index.js +1 -0
- package/dist/src/Form/components/FormMessage/FormMessage.js +48 -0
- package/dist/src/Form/components/FormMessage/components/InternalFormMessage/InternalFormMessage.js +28 -0
- package/dist/src/Form/components/FormMessage/components/InternalFormMessage/InternalFormMessage.style.js +17 -0
- package/dist/src/Form/components/FormMessage/components/InternalFormMessage/index.js +1 -0
- package/dist/src/Form/components/FormMessage/components/InternalFormMessage/messages.js +8 -0
- package/dist/src/Form/components/FormMessage/index.js +1 -0
- package/dist/src/Form/components/FormMessageBanner/FormMessageBanner.js +15 -0
- package/dist/src/Form/components/FormMessageBanner/index.js +1 -0
- package/dist/src/Form/components/FormSaveButton/FormSaveButton.js +69 -0
- package/dist/src/Form/components/FormSaveButton/index.js +1 -0
- package/dist/src/Form/components/FormSaveButton/messages.js +8 -0
- package/dist/src/Form/constants.js +2 -0
- package/dist/src/Form/context/AtlantisFormContext.js +16 -0
- package/dist/src/Form/context/index.js +1 -0
- package/dist/src/Form/context/types.js +1 -0
- package/dist/src/Form/hooks/useFormViewRefs.js +14 -0
- package/dist/src/Form/hooks/useInternalForm.js +37 -0
- package/dist/src/Form/hooks/useOfflineHandler.js +24 -0
- package/dist/src/Form/hooks/useSaveButtonPosition.js +25 -0
- package/dist/src/Form/hooks/useScreenInformation.js +15 -0
- package/dist/src/Form/hooks/useScrollToError/index.js +1 -0
- package/dist/src/Form/hooks/useScrollToError/useScrollToError.js +63 -0
- package/dist/src/Form/index.js +4 -0
- package/dist/src/Form/messages.js +28 -0
- package/dist/src/Form/types.js +10 -0
- package/dist/src/InputDate/InputDate.js +76 -0
- package/dist/src/InputDate/index.js +1 -0
- package/dist/src/InputDate/messages.js +8 -0
- package/dist/src/Menu/Menu.js +67 -0
- package/dist/src/Menu/Menu.style.js +6 -0
- package/dist/src/Menu/components/MenuOption/MenuOption.js +25 -0
- package/dist/src/Menu/components/MenuOption/MenuOption.style.js +10 -0
- package/dist/src/Menu/components/MenuOption/index.js +1 -0
- package/dist/src/Menu/components/Overlay/Overlay.js +9 -0
- package/dist/src/Menu/components/Overlay/Overlay.style.js +6 -0
- package/dist/src/Menu/components/Overlay/index.js +1 -0
- package/dist/src/Menu/index.js +1 -0
- package/dist/src/Menu/messages.js +8 -0
- package/dist/src/Menu/types.js +1 -0
- package/dist/src/Menu/utils.js +84 -0
- package/dist/src/index.js +3 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/src/AtlantisContext/AtlantisContext.d.ts +7 -1
- package/dist/types/src/Form/Form.d.ts +4 -0
- package/dist/types/src/Form/Form.style.d.ts +31 -0
- package/dist/types/src/Form/components/FormActionBar/FormActionBar.d.ts +13 -0
- package/dist/types/src/Form/components/FormActionBar/FormActionBar.style.d.ts +15 -0
- package/dist/types/src/Form/components/FormActionBar/index.d.ts +2 -0
- package/dist/types/src/Form/components/FormBody/FormBody.d.ts +10 -0
- package/dist/types/src/Form/components/FormBody/FormBody.style.d.ts +24 -0
- package/dist/types/src/Form/components/FormBody/index.d.ts +1 -0
- package/dist/types/src/Form/components/FormCache/FormCache.d.ts +10 -0
- package/dist/types/src/Form/components/FormErrorBanner/FormErrorBanner.d.ts +3 -0
- package/dist/types/src/Form/components/FormErrorBanner/index.d.ts +1 -0
- package/dist/types/src/Form/components/FormErrorBanner/messages.d.ts +12 -0
- package/dist/types/src/Form/components/FormMask/FormMask.d.ts +2 -0
- package/dist/types/src/Form/components/FormMask/FormMask.style.d.ts +13 -0
- package/dist/types/src/Form/components/FormMask/index.d.ts +1 -0
- package/dist/types/src/Form/components/FormMessage/FormMessage.d.ts +19 -0
- package/dist/types/src/Form/components/FormMessage/components/InternalFormMessage/InternalFormMessage.d.ts +8 -0
- package/dist/types/src/Form/components/FormMessage/components/InternalFormMessage/InternalFormMessage.style.d.ts +20 -0
- package/dist/types/src/Form/components/FormMessage/components/InternalFormMessage/index.d.ts +1 -0
- package/dist/types/src/Form/components/FormMessage/components/InternalFormMessage/messages.d.ts +7 -0
- package/dist/types/src/Form/components/FormMessage/index.d.ts +1 -0
- package/dist/types/src/Form/components/FormMessageBanner/FormMessageBanner.d.ts +7 -0
- package/dist/types/src/Form/components/FormMessageBanner/index.d.ts +1 -0
- package/dist/types/src/Form/components/FormSaveButton/FormSaveButton.d.ts +3 -0
- package/dist/types/src/Form/components/FormSaveButton/index.d.ts +1 -0
- package/dist/types/src/Form/components/FormSaveButton/messages.d.ts +7 -0
- package/dist/types/src/Form/constants.d.ts +2 -0
- package/dist/types/src/Form/context/AtlantisFormContext.d.ts +12 -0
- package/dist/types/src/Form/context/index.d.ts +2 -0
- package/dist/types/src/Form/context/types.d.ts +26 -0
- package/dist/types/src/Form/hooks/useFormViewRefs.d.ts +10 -0
- package/dist/types/src/Form/hooks/useInternalForm.d.ts +19 -0
- package/dist/types/src/Form/hooks/useOfflineHandler.d.ts +1 -0
- package/dist/types/src/Form/hooks/useSaveButtonPosition.d.ts +12 -0
- package/dist/types/src/Form/hooks/useScreenInformation.d.ts +8 -0
- package/dist/types/src/Form/hooks/useScrollToError/index.d.ts +1 -0
- package/dist/types/src/Form/hooks/useScrollToError/useScrollToError.d.ts +10 -0
- package/dist/types/src/Form/index.d.ts +5 -0
- package/dist/types/src/Form/messages.d.ts +27 -0
- package/dist/types/src/Form/types.d.ts +199 -0
- package/dist/types/src/InputDate/InputDate.d.ts +74 -0
- package/dist/types/src/InputDate/index.d.ts +1 -0
- package/dist/types/src/InputDate/messages.d.ts +7 -0
- package/dist/types/src/InputNumber/InputNumber.d.ts +1 -1
- package/dist/types/src/Menu/Menu.d.ts +3 -0
- package/dist/types/src/Menu/Menu.style.d.ts +18 -0
- package/dist/types/src/Menu/components/MenuOption/MenuOption.d.ts +3 -0
- package/dist/types/src/Menu/components/MenuOption/MenuOption.style.d.ts +8 -0
- package/dist/types/src/Menu/components/MenuOption/index.d.ts +1 -0
- package/dist/types/src/Menu/components/Overlay/Overlay.d.ts +3 -0
- package/dist/types/src/Menu/components/Overlay/Overlay.style.d.ts +12 -0
- package/dist/types/src/Menu/components/Overlay/index.d.ts +1 -0
- package/dist/types/src/Menu/index.d.ts +2 -0
- package/dist/types/src/Menu/messages.d.ts +7 -0
- package/dist/types/src/Menu/types.d.ts +22 -0
- package/dist/types/src/Menu/utils.d.ts +10 -0
- package/dist/types/src/index.d.ts +3 -0
- package/package.json +3 -2
- package/src/AtlantisContext/AtlantisContext.tsx +10 -1
- package/src/Form/Form.style.ts +34 -0
- package/src/Form/Form.test.tsx +588 -0
- package/src/Form/Form.tsx +296 -0
- package/src/Form/components/FormActionBar/FormActionBar.style.ts +11 -0
- package/src/Form/components/FormActionBar/FormActionBar.tsx +63 -0
- package/src/Form/components/FormActionBar/index.ts +2 -0
- package/src/Form/components/FormBody/FormBody.style.ts +27 -0
- package/src/Form/components/FormBody/FormBody.tsx +62 -0
- package/src/Form/components/FormBody/index.ts +1 -0
- package/src/Form/components/FormCache/FormCache.tsx +50 -0
- package/src/Form/components/FormErrorBanner/FormErrorBanner.test.tsx +124 -0
- package/src/Form/components/FormErrorBanner/FormErrorBanner.tsx +34 -0
- package/src/Form/components/FormErrorBanner/index.ts +1 -0
- package/src/Form/components/FormErrorBanner/messages.ts +14 -0
- package/src/Form/components/FormMask/FormMask.style.tsx +16 -0
- package/src/Form/components/FormMask/FormMask.tsx +19 -0
- package/src/Form/components/FormMask/index.ts +1 -0
- package/src/Form/components/FormMessage/FormMessage.test.tsx +72 -0
- package/src/Form/components/FormMessage/FormMessage.tsx +63 -0
- package/src/Form/components/FormMessage/components/InternalFormMessage/InternalFormMessage.style.ts +18 -0
- package/src/Form/components/FormMessage/components/InternalFormMessage/InternalFormMessage.tsx +55 -0
- package/src/Form/components/FormMessage/components/InternalFormMessage/index.ts +1 -0
- package/src/Form/components/FormMessage/components/InternalFormMessage/messages.ts +10 -0
- package/src/Form/components/FormMessage/index.ts +1 -0
- package/src/Form/components/FormMessageBanner/FormMessageBanner.test.tsx +27 -0
- package/src/Form/components/FormMessageBanner/FormMessageBanner.tsx +33 -0
- package/src/Form/components/FormMessageBanner/index.ts +1 -0
- package/src/Form/components/FormSaveButton/FormSaveButton.test.tsx +159 -0
- package/src/Form/components/FormSaveButton/FormSaveButton.tsx +103 -0
- package/src/Form/components/FormSaveButton/index.ts +1 -0
- package/src/Form/components/FormSaveButton/messages.ts +9 -0
- package/src/Form/constants.ts +2 -0
- package/src/Form/context/AtlantisFormContext.test.tsx +45 -0
- package/src/Form/context/AtlantisFormContext.tsx +21 -0
- package/src/Form/context/index.ts +5 -0
- package/src/Form/context/types.ts +34 -0
- package/src/Form/hooks/useFormViewRefs.ts +23 -0
- package/src/Form/hooks/useInternalForm.ts +99 -0
- package/src/Form/hooks/useOfflineHandler.ts +36 -0
- package/src/Form/hooks/useSaveButtonPosition.ts +52 -0
- package/src/Form/hooks/useScreenInformation.ts +25 -0
- package/src/Form/hooks/useScrollToError/index.ts +1 -0
- package/src/Form/hooks/useScrollToError/useScrollToError.test.tsx +103 -0
- package/src/Form/hooks/useScrollToError/useScrollToError.ts +102 -0
- package/src/Form/index.ts +13 -0
- package/src/Form/messages.ts +33 -0
- package/src/Form/types.ts +255 -0
- package/src/InputDate/InputDate.test.tsx +295 -0
- package/src/InputDate/InputDate.tsx +231 -0
- package/src/InputDate/index.ts +1 -0
- package/src/InputDate/messages.ts +9 -0
- package/src/InputNumber/InputNumber.tsx +1 -1
- package/src/Menu/Menu.style.ts +16 -0
- package/src/Menu/Menu.test.tsx +201 -0
- package/src/Menu/Menu.tsx +116 -0
- package/src/Menu/components/MenuOption/MenuOption.style.tsx +11 -0
- package/src/Menu/components/MenuOption/MenuOption.tsx +63 -0
- package/src/Menu/components/MenuOption/index.ts +1 -0
- package/src/Menu/components/Overlay/Overlay.style.ts +13 -0
- package/src/Menu/components/Overlay/Overlay.tsx +16 -0
- package/src/Menu/components/Overlay/index.ts +1 -0
- package/src/Menu/index.ts +6 -0
- package/src/Menu/messages.ts +9 -0
- package/src/Menu/types.ts +25 -0
- package/src/Menu/utils.ts +151 -0
- package/src/index.ts +3 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { MutableRefObject } from "react";
|
|
2
|
+
import { FieldValues, UseFormReturn } from "react-hook-form";
|
|
3
|
+
|
|
4
|
+
export interface UseConfirmBeforeBackProps {
|
|
5
|
+
alwaysPreventBack: boolean;
|
|
6
|
+
onAcceptEvent?: () => void;
|
|
7
|
+
shouldShowAlert: boolean;
|
|
8
|
+
showLostProgressMessage?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface LocalCacheOptions {
|
|
12
|
+
/**
|
|
13
|
+
* Allows the ability to specify that the cached data contains pre-filled data.
|
|
14
|
+
* By setting this the cached data will only be applied if the same id is based
|
|
15
|
+
* for the same key. If the id doesn't match the data will be deleted from the
|
|
16
|
+
* cache.
|
|
17
|
+
*/
|
|
18
|
+
readonly id?: string | string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface AtlantisFormContextProps {
|
|
22
|
+
useConfirmBeforeBack: (
|
|
23
|
+
props: UseConfirmBeforeBackProps,
|
|
24
|
+
) => MutableRefObject<() => void>;
|
|
25
|
+
useInternalFormLocalCache: <TData extends FieldValues>(
|
|
26
|
+
formMethods: UseFormReturn<TData>,
|
|
27
|
+
cacheKey?: string,
|
|
28
|
+
options?: LocalCacheOptions,
|
|
29
|
+
) => {
|
|
30
|
+
setLocalCache: (data: TData) => void;
|
|
31
|
+
removeLocalCache: () => void;
|
|
32
|
+
};
|
|
33
|
+
headerHeight: number;
|
|
34
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { RefObject, useCallback, useRef } from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
|
|
4
|
+
|
|
5
|
+
interface UseFormViewRefsReturn {
|
|
6
|
+
readonly scrollViewRef: RefObject<KeyboardAwareScrollView>;
|
|
7
|
+
readonly bottomViewRef: RefObject<View>;
|
|
8
|
+
readonly scrollToTop: () => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function useFormViewRefs(): UseFormViewRefsReturn {
|
|
12
|
+
const scrollViewRef: RefObject<KeyboardAwareScrollView> =
|
|
13
|
+
useRef<KeyboardAwareScrollView>(null);
|
|
14
|
+
const bottomViewRef: RefObject<View> = useRef<View>(null);
|
|
15
|
+
const scrollToTop = useCallback(() => {
|
|
16
|
+
scrollViewRef.current?.scrollToPosition(0, 0);
|
|
17
|
+
}, [scrollViewRef]);
|
|
18
|
+
return {
|
|
19
|
+
scrollViewRef: scrollViewRef,
|
|
20
|
+
bottomViewRef,
|
|
21
|
+
scrollToTop,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FieldValues,
|
|
3
|
+
UseFormHandleSubmit,
|
|
4
|
+
UseFormReturn,
|
|
5
|
+
useForm,
|
|
6
|
+
} from "react-hook-form";
|
|
7
|
+
import { MutableRefObject, RefObject } from "react";
|
|
8
|
+
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
|
|
9
|
+
import { useAtlantisContext } from "../../AtlantisContext";
|
|
10
|
+
import { useAtlantisFormContext } from "../context/AtlantisFormContext";
|
|
11
|
+
import { InternalFormProps } from "../types";
|
|
12
|
+
|
|
13
|
+
type UseInternalFormProps<T extends FieldValues, SubmitResponseType> = Pick<
|
|
14
|
+
InternalFormProps<T, SubmitResponseType>,
|
|
15
|
+
| "mode"
|
|
16
|
+
| "reValidateMode"
|
|
17
|
+
| "initialValues"
|
|
18
|
+
| "formRef"
|
|
19
|
+
| "localCacheKey"
|
|
20
|
+
| "localCacheExclude"
|
|
21
|
+
| "localCacheId"
|
|
22
|
+
> & {
|
|
23
|
+
scrollViewRef?: RefObject<KeyboardAwareScrollView>;
|
|
24
|
+
readonly saveButtonHeight: number;
|
|
25
|
+
readonly messageBannerHeight: number;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
interface UseInternalForm<T extends FieldValues> {
|
|
29
|
+
readonly formMethods: UseFormReturn<T>;
|
|
30
|
+
readonly handleSubmit: UseFormHandleSubmit<T>;
|
|
31
|
+
readonly isSubmitting: boolean;
|
|
32
|
+
readonly isDirty: boolean;
|
|
33
|
+
readonly removeListenerRef: MutableRefObject<() => void>;
|
|
34
|
+
readonly setLocalCache: (data: T) => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function useInternalForm<T extends FieldValues, SubmitResponseType>({
|
|
38
|
+
mode,
|
|
39
|
+
reValidateMode,
|
|
40
|
+
initialValues,
|
|
41
|
+
formRef,
|
|
42
|
+
localCacheKey,
|
|
43
|
+
localCacheId,
|
|
44
|
+
scrollViewRef,
|
|
45
|
+
saveButtonHeight,
|
|
46
|
+
messageBannerHeight,
|
|
47
|
+
}: UseInternalFormProps<T, SubmitResponseType>): UseInternalForm<T> {
|
|
48
|
+
const { useConfirmBeforeBack, useInternalFormLocalCache } =
|
|
49
|
+
useAtlantisFormContext();
|
|
50
|
+
|
|
51
|
+
const { isOnline } = useAtlantisContext();
|
|
52
|
+
|
|
53
|
+
const formMethods = useForm<T>({
|
|
54
|
+
mode,
|
|
55
|
+
reValidateMode,
|
|
56
|
+
defaultValues: initialValues,
|
|
57
|
+
shouldFocusError: false,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const clientSideSaveOn = localCacheKey && localCacheKey !== "INVALID";
|
|
61
|
+
|
|
62
|
+
const { setLocalCache, removeLocalCache } = useInternalFormLocalCache(
|
|
63
|
+
formMethods,
|
|
64
|
+
localCacheKey,
|
|
65
|
+
{
|
|
66
|
+
id: localCacheId,
|
|
67
|
+
},
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const {
|
|
71
|
+
handleSubmit,
|
|
72
|
+
formState: { isSubmitting, isDirty },
|
|
73
|
+
} = formMethods;
|
|
74
|
+
|
|
75
|
+
if (formRef) {
|
|
76
|
+
formRef.current = {
|
|
77
|
+
...formMethods,
|
|
78
|
+
saveButtonHeight,
|
|
79
|
+
messageBannerHeight,
|
|
80
|
+
scrollViewRef,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const removeListenerRef = useConfirmBeforeBack({
|
|
85
|
+
alwaysPreventBack: isSubmitting,
|
|
86
|
+
shouldShowAlert: isDirty,
|
|
87
|
+
onAcceptEvent: isOnline ? removeLocalCache : undefined,
|
|
88
|
+
showLostProgressMessage: isOnline || !clientSideSaveOn ? true : false,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
setLocalCache,
|
|
93
|
+
formMethods,
|
|
94
|
+
handleSubmit,
|
|
95
|
+
isSubmitting,
|
|
96
|
+
isDirty,
|
|
97
|
+
removeListenerRef,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
import { useIntl } from "react-intl";
|
|
3
|
+
import { Alert } from "react-native";
|
|
4
|
+
import { messages } from "../messages";
|
|
5
|
+
|
|
6
|
+
export function useOfflineHandler(): (
|
|
7
|
+
callback: () => void,
|
|
8
|
+
dismiss: () => void,
|
|
9
|
+
) => () => void {
|
|
10
|
+
const { formatMessage } = useIntl();
|
|
11
|
+
|
|
12
|
+
const handleOfflineSubmit = useCallback(
|
|
13
|
+
(callback: () => void, dismiss: () => void) => {
|
|
14
|
+
return () => {
|
|
15
|
+
Alert.alert(
|
|
16
|
+
formatMessage(messages.unavailableNetworkTitle),
|
|
17
|
+
formatMessage(messages.unavailableNetworkMessage),
|
|
18
|
+
[
|
|
19
|
+
{
|
|
20
|
+
text: formatMessage(messages.dismissAlertButton),
|
|
21
|
+
style: "cancel",
|
|
22
|
+
onPress: dismiss,
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
text: formatMessage(messages.retryAlertButton),
|
|
26
|
+
style: "default",
|
|
27
|
+
onPress: callback,
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
);
|
|
31
|
+
};
|
|
32
|
+
},
|
|
33
|
+
[formatMessage],
|
|
34
|
+
);
|
|
35
|
+
return handleOfflineSubmit;
|
|
36
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Platform } from "react-native";
|
|
2
|
+
import { useScreenInformation } from "./useScreenInformation";
|
|
3
|
+
import { tokens } from "../../utils/design";
|
|
4
|
+
|
|
5
|
+
interface UseSaveButtonPositionReturn {
|
|
6
|
+
saveButtonPosition: "sticky" | "inline";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface UseSaveButtonPositionParams {
|
|
10
|
+
formContentHeight: number;
|
|
11
|
+
isBottomSheetOpen: boolean;
|
|
12
|
+
showStickySaveButton: boolean;
|
|
13
|
+
keyboardHeight: number;
|
|
14
|
+
keyboardScreenY: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function useSaveButtonPosition({
|
|
18
|
+
formContentHeight,
|
|
19
|
+
isBottomSheetOpen,
|
|
20
|
+
showStickySaveButton,
|
|
21
|
+
keyboardHeight,
|
|
22
|
+
keyboardScreenY,
|
|
23
|
+
}: UseSaveButtonPositionParams): UseSaveButtonPositionReturn {
|
|
24
|
+
const { headerHeight } = useScreenInformation();
|
|
25
|
+
|
|
26
|
+
if (showStickySaveButton) {
|
|
27
|
+
return {
|
|
28
|
+
saveButtonPosition: "sticky",
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (Platform.OS === "android" || isBottomSheetOpen) {
|
|
33
|
+
return {
|
|
34
|
+
saveButtonPosition: "inline",
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const isKeyboardOpen = Boolean(keyboardHeight);
|
|
39
|
+
|
|
40
|
+
// tokens["space-large"] acts like a safe space below the form to avoid
|
|
41
|
+
// rendering the sticky button above the form elements.
|
|
42
|
+
const calculatedFormContentHeight =
|
|
43
|
+
formContentHeight + headerHeight + tokens["space-large"];
|
|
44
|
+
|
|
45
|
+
const isKeyboardOverForm = calculatedFormContentHeight > keyboardScreenY;
|
|
46
|
+
|
|
47
|
+
const showInlineSaveButton = isKeyboardOpen && isKeyboardOverForm;
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
saveButtonPosition: showInlineSaveButton ? "inline" : "sticky",
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useWindowDimensions } from "react-native";
|
|
2
|
+
import { EdgeInsets, useSafeAreaInsets } from "react-native-safe-area-context";
|
|
3
|
+
import { KEYBOARD_TOP_PADDING_AUTO_SCROLL } from "../constants";
|
|
4
|
+
import { useAtlantisFormContext } from "../context";
|
|
5
|
+
|
|
6
|
+
interface UseScreenInformation {
|
|
7
|
+
readonly windowHeight: number;
|
|
8
|
+
readonly headerHeight: number;
|
|
9
|
+
readonly insets: EdgeInsets;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function useScreenInformation(): UseScreenInformation {
|
|
13
|
+
const { headerHeight } = useAtlantisFormContext();
|
|
14
|
+
const windowHeight = useWindowDimensions().height;
|
|
15
|
+
const headerHeightWithPadding =
|
|
16
|
+
headerHeight + KEYBOARD_TOP_PADDING_AUTO_SCROLL;
|
|
17
|
+
|
|
18
|
+
const insets = useSafeAreaInsets();
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
windowHeight,
|
|
22
|
+
headerHeight: headerHeightWithPadding,
|
|
23
|
+
insets,
|
|
24
|
+
} as const;
|
|
25
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./useScrollToError";
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { renderHook } from "@testing-library/react-hooks";
|
|
2
|
+
import { useScrollToError } from "./useScrollToError";
|
|
3
|
+
|
|
4
|
+
const mockFormState = {
|
|
5
|
+
isDirty: true,
|
|
6
|
+
dirtyFields: {},
|
|
7
|
+
isSubmitted: false,
|
|
8
|
+
isSubmitSuccessful: false,
|
|
9
|
+
submitCount: 0,
|
|
10
|
+
touchedFields: {},
|
|
11
|
+
isSubmitting: false,
|
|
12
|
+
isValidating: false,
|
|
13
|
+
isValid: true,
|
|
14
|
+
errors: {},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const mockScreenReaderEnabled = jest.fn().mockReturnValue(false);
|
|
18
|
+
jest.mock("../../../hooks/useIsScreenReaderEnabled", () => ({
|
|
19
|
+
useIsScreenReaderEnabled: () => mockScreenReaderEnabled(),
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
jest.mock("../../../ErrorMessageWrapper", () => ({
|
|
23
|
+
useErrorMessageContext: () => ({
|
|
24
|
+
elements: {
|
|
25
|
+
el: {
|
|
26
|
+
measure: jest.fn((_, callback) => callback()),
|
|
27
|
+
hasErrorMessage: true,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
register: jest.fn(),
|
|
31
|
+
unregister: jest.fn(),
|
|
32
|
+
}),
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
const handleScrollToPosition = jest.fn();
|
|
36
|
+
const handleSetFocus = jest.fn();
|
|
37
|
+
|
|
38
|
+
const initialProps = {
|
|
39
|
+
formState: mockFormState,
|
|
40
|
+
refNode: 1,
|
|
41
|
+
scrollToPosition: handleScrollToPosition,
|
|
42
|
+
setFocus: handleSetFocus,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
handleScrollToPosition.mockClear();
|
|
47
|
+
handleSetFocus.mockClear();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("useScrollToError", () => {
|
|
51
|
+
it("should do nothing if everything is valid", () => {
|
|
52
|
+
renderHook(useScrollToError, { initialProps });
|
|
53
|
+
|
|
54
|
+
expect(handleSetFocus).not.toHaveBeenCalled();
|
|
55
|
+
expect(handleScrollToPosition).not.toHaveBeenCalled();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("should focus with RHF if it can", () => {
|
|
59
|
+
const { rerender } = renderHook(useScrollToError, { initialProps });
|
|
60
|
+
rerender({
|
|
61
|
+
...initialProps,
|
|
62
|
+
formState: { ...mockFormState, isValid: false, submitCount: 1 },
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
expect(handleSetFocus).toHaveBeenCalled();
|
|
66
|
+
expect(handleScrollToPosition).not.toHaveBeenCalled();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("should manually scroll", () => {
|
|
70
|
+
// @ts-expect-error - making this fail when it gets called since you can't
|
|
71
|
+
// call undefined as a function. This mimic's RHF not being able to focus on
|
|
72
|
+
// non-input fields
|
|
73
|
+
const failedSetFocus = jest.fn(() => failingFn());
|
|
74
|
+
const manualScrollProps = { ...initialProps, setFocus: failedSetFocus };
|
|
75
|
+
|
|
76
|
+
const { rerender } = renderHook(useScrollToError, {
|
|
77
|
+
initialProps: manualScrollProps,
|
|
78
|
+
});
|
|
79
|
+
rerender({
|
|
80
|
+
...manualScrollProps,
|
|
81
|
+
formState: { ...mockFormState, isValid: false, submitCount: 1 },
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
expect(failedSetFocus).toHaveBeenCalled();
|
|
85
|
+
expect(failedSetFocus).toThrow();
|
|
86
|
+
expect(handleScrollToPosition).toHaveBeenCalled();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe("With screen readers", () => {
|
|
90
|
+
it("should not fire the setFocus", () => {
|
|
91
|
+
mockScreenReaderEnabled.mockReturnValue(true);
|
|
92
|
+
|
|
93
|
+
const { rerender } = renderHook(useScrollToError, { initialProps });
|
|
94
|
+
rerender({
|
|
95
|
+
...initialProps,
|
|
96
|
+
formState: { ...mockFormState, isValid: false, submitCount: 1 },
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
expect(handleSetFocus).not.toHaveBeenCalled();
|
|
100
|
+
expect(handleScrollToPosition).toHaveBeenCalled();
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { useCallback, useState } from "react";
|
|
2
|
+
import { FieldValues, FormState, Path, UseFormSetFocus } from "react-hook-form";
|
|
3
|
+
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
|
|
4
|
+
import {
|
|
5
|
+
Keyboard,
|
|
6
|
+
MeasureInWindowOnSuccessCallback,
|
|
7
|
+
Platform,
|
|
8
|
+
} from "react-native";
|
|
9
|
+
import { useIsScreenReaderEnabled } from "../../../hooks/useIsScreenReaderEnabled";
|
|
10
|
+
import { useErrorMessageContext } from "../../../ErrorMessageWrapper";
|
|
11
|
+
|
|
12
|
+
interface UseScrollToErrorParams<T extends FieldValues> {
|
|
13
|
+
readonly formState: FormState<T>;
|
|
14
|
+
readonly refNode: number | null;
|
|
15
|
+
readonly setFocus: UseFormSetFocus<T>;
|
|
16
|
+
readonly scrollToPosition?: KeyboardAwareScrollView["scrollToPosition"];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function useScrollToError<T extends FieldValues>({
|
|
20
|
+
formState: { errors, isValid, submitCount },
|
|
21
|
+
refNode,
|
|
22
|
+
setFocus,
|
|
23
|
+
scrollToPosition,
|
|
24
|
+
}: UseScrollToErrorParams<T>): void {
|
|
25
|
+
const [submitCounter, setSubmitCounter] = useState(submitCount);
|
|
26
|
+
const isScreenReaderEnabled = useIsScreenReaderEnabled();
|
|
27
|
+
const manuallyScrollToElement = useManuallyScrollToElement(
|
|
28
|
+
handleScroll,
|
|
29
|
+
refNode,
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
// Determine if the form has been submitted by checking if the submit count
|
|
33
|
+
// went up.
|
|
34
|
+
const hasBeenSubmitted = submitCounter < submitCount;
|
|
35
|
+
if (!hasBeenSubmitted) return;
|
|
36
|
+
if (isScreenReaderEnabled) {
|
|
37
|
+
manuallyScrollToElement();
|
|
38
|
+
Keyboard.dismiss();
|
|
39
|
+
} else {
|
|
40
|
+
defaultAutoScroll();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
setSubmitCounter(submitCount);
|
|
44
|
+
|
|
45
|
+
function defaultAutoScroll() {
|
|
46
|
+
if (isValid) return;
|
|
47
|
+
try {
|
|
48
|
+
focusInputWithRHF(errors, setFocus);
|
|
49
|
+
} catch {
|
|
50
|
+
manuallyScrollToElement();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function handleScroll(
|
|
55
|
+
...[x, y]: Parameters<MeasureInWindowOnSuccessCallback>
|
|
56
|
+
) {
|
|
57
|
+
/**
|
|
58
|
+
* Disable scroll animation on android when screen reader is active since it
|
|
59
|
+
* can't accessibility-focus on an offscreen component.
|
|
60
|
+
*/
|
|
61
|
+
const isAndroidWithScreenReader =
|
|
62
|
+
isScreenReaderEnabled && Platform.OS === "android";
|
|
63
|
+
const shouldAnimateScroll = !isAndroidWithScreenReader;
|
|
64
|
+
|
|
65
|
+
scrollToPosition?.(x, y, shouldAnimateScroll);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function focusInputWithRHF<T extends FieldValues>(
|
|
70
|
+
errors: FormState<T>["errors"],
|
|
71
|
+
setFocus: UseFormSetFocus<T>,
|
|
72
|
+
) {
|
|
73
|
+
const errorMessages = Object.keys(errors);
|
|
74
|
+
setFocus(errorMessages[0] as Path<T>);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Manually scroll to the element by checking which elements has an error from
|
|
79
|
+
* the Error Message Context
|
|
80
|
+
*/
|
|
81
|
+
function useManuallyScrollToElement(
|
|
82
|
+
callback: MeasureInWindowOnSuccessCallback,
|
|
83
|
+
refNode: number | null,
|
|
84
|
+
) {
|
|
85
|
+
const { elements } = useErrorMessageContext();
|
|
86
|
+
|
|
87
|
+
return useCallback(() => {
|
|
88
|
+
const elementWithError = Object.keys(elements).find(
|
|
89
|
+
el => elements[el].hasErrorMessage,
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
if (elementWithError) {
|
|
93
|
+
const element = elements[elementWithError];
|
|
94
|
+
refNode && element.measure(refNode, callback, handleError);
|
|
95
|
+
element.accessibilityFocus();
|
|
96
|
+
}
|
|
97
|
+
}, [callback, elements, refNode]);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function handleError() {
|
|
101
|
+
return Error("Couldn't scroll to error");
|
|
102
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { Form } from "./Form";
|
|
2
|
+
export { FormMessage } from "./components/FormMessage";
|
|
3
|
+
export type {
|
|
4
|
+
FormBannerErrors,
|
|
5
|
+
FormBannerMessage,
|
|
6
|
+
FormErrors,
|
|
7
|
+
FormRef,
|
|
8
|
+
FormValues,
|
|
9
|
+
SecondaryActionProp,
|
|
10
|
+
ValidationRulesByFieldPath,
|
|
11
|
+
} from "./types";
|
|
12
|
+
export { FormSubmitErrorType, FormBannerMessageType } from "./types";
|
|
13
|
+
export * from "./context";
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { defineMessages } from "react-intl";
|
|
2
|
+
|
|
3
|
+
export const messages = defineMessages({
|
|
4
|
+
loadingA11YLabel: {
|
|
5
|
+
id: "loadingA11yLabel",
|
|
6
|
+
defaultMessage: "Loading",
|
|
7
|
+
description: "Accessibility label for the loading indicator",
|
|
8
|
+
},
|
|
9
|
+
|
|
10
|
+
dismissAlertButton: {
|
|
11
|
+
id: "dismiss",
|
|
12
|
+
defaultMessage: "Dismiss",
|
|
13
|
+
description: "The label for the button to dismiss the alert ",
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
retryAlertButton: {
|
|
17
|
+
id: "retry",
|
|
18
|
+
defaultMessage: "Try Again",
|
|
19
|
+
description: "The label for the alert button to try action again",
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
unavailableNetworkTitle: {
|
|
23
|
+
id: "unavailableNetworkTitle",
|
|
24
|
+
defaultMessage: "Network unavailable",
|
|
25
|
+
description: "The title for alert about network unavailable",
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
unavailableNetworkMessage: {
|
|
29
|
+
id: "unavailableNetworkMessage",
|
|
30
|
+
defaultMessage: "Check your internet connection and try again later.",
|
|
31
|
+
description: "The message for alert about network unavailable",
|
|
32
|
+
},
|
|
33
|
+
});
|