@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,16 @@
|
|
|
1
|
+
import { createContext, useContext, useRef } from "react";
|
|
2
|
+
export const defaultValues = {
|
|
3
|
+
useConfirmBeforeBack: () => {
|
|
4
|
+
const ref = useRef(() => undefined);
|
|
5
|
+
return ref;
|
|
6
|
+
},
|
|
7
|
+
useInternalFormLocalCache: () => ({
|
|
8
|
+
setLocalCache: () => undefined,
|
|
9
|
+
removeLocalCache: () => undefined,
|
|
10
|
+
}),
|
|
11
|
+
headerHeight: 0,
|
|
12
|
+
};
|
|
13
|
+
export const AtlantisFormContext = createContext(defaultValues);
|
|
14
|
+
export function useAtlantisFormContext() {
|
|
15
|
+
return useContext(AtlantisFormContext);
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { AtlantisFormContext, useAtlantisFormContext, } from "./AtlantisFormContext";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { useCallback, useRef } from "react";
|
|
2
|
+
export function useFormViewRefs() {
|
|
3
|
+
const scrollViewRef = useRef(null);
|
|
4
|
+
const bottomViewRef = useRef(null);
|
|
5
|
+
const scrollToTop = useCallback(() => {
|
|
6
|
+
var _a;
|
|
7
|
+
(_a = scrollViewRef.current) === null || _a === void 0 ? void 0 : _a.scrollToPosition(0, 0);
|
|
8
|
+
}, [scrollViewRef]);
|
|
9
|
+
return {
|
|
10
|
+
scrollViewRef: scrollViewRef,
|
|
11
|
+
bottomViewRef,
|
|
12
|
+
scrollToTop,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useForm, } from "react-hook-form";
|
|
2
|
+
import { useAtlantisContext } from "../../AtlantisContext";
|
|
3
|
+
import { useAtlantisFormContext } from "../context/AtlantisFormContext";
|
|
4
|
+
export function useInternalForm({ mode, reValidateMode, initialValues, formRef, localCacheKey, localCacheId, scrollViewRef, saveButtonHeight, messageBannerHeight, }) {
|
|
5
|
+
const { useConfirmBeforeBack, useInternalFormLocalCache } = useAtlantisFormContext();
|
|
6
|
+
const { isOnline } = useAtlantisContext();
|
|
7
|
+
const formMethods = useForm({
|
|
8
|
+
mode,
|
|
9
|
+
reValidateMode,
|
|
10
|
+
defaultValues: initialValues,
|
|
11
|
+
shouldFocusError: false,
|
|
12
|
+
});
|
|
13
|
+
const clientSideSaveOn = localCacheKey && localCacheKey !== "INVALID";
|
|
14
|
+
const { setLocalCache, removeLocalCache } = useInternalFormLocalCache(formMethods, localCacheKey, {
|
|
15
|
+
id: localCacheId,
|
|
16
|
+
});
|
|
17
|
+
const { handleSubmit, formState: { isSubmitting, isDirty }, } = formMethods;
|
|
18
|
+
if (formRef) {
|
|
19
|
+
formRef.current = Object.assign(Object.assign({}, formMethods), { saveButtonHeight,
|
|
20
|
+
messageBannerHeight,
|
|
21
|
+
scrollViewRef });
|
|
22
|
+
}
|
|
23
|
+
const removeListenerRef = useConfirmBeforeBack({
|
|
24
|
+
alwaysPreventBack: isSubmitting,
|
|
25
|
+
shouldShowAlert: isDirty,
|
|
26
|
+
onAcceptEvent: isOnline ? removeLocalCache : undefined,
|
|
27
|
+
showLostProgressMessage: isOnline || !clientSideSaveOn ? true : false,
|
|
28
|
+
});
|
|
29
|
+
return {
|
|
30
|
+
setLocalCache,
|
|
31
|
+
formMethods,
|
|
32
|
+
handleSubmit,
|
|
33
|
+
isSubmitting,
|
|
34
|
+
isDirty,
|
|
35
|
+
removeListenerRef,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
import { useIntl } from "react-intl";
|
|
3
|
+
import { Alert } from "react-native";
|
|
4
|
+
import { messages } from "../messages";
|
|
5
|
+
export function useOfflineHandler() {
|
|
6
|
+
const { formatMessage } = useIntl();
|
|
7
|
+
const handleOfflineSubmit = useCallback((callback, dismiss) => {
|
|
8
|
+
return () => {
|
|
9
|
+
Alert.alert(formatMessage(messages.unavailableNetworkTitle), formatMessage(messages.unavailableNetworkMessage), [
|
|
10
|
+
{
|
|
11
|
+
text: formatMessage(messages.dismissAlertButton),
|
|
12
|
+
style: "cancel",
|
|
13
|
+
onPress: dismiss,
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
text: formatMessage(messages.retryAlertButton),
|
|
17
|
+
style: "default",
|
|
18
|
+
onPress: callback,
|
|
19
|
+
},
|
|
20
|
+
]);
|
|
21
|
+
};
|
|
22
|
+
}, [formatMessage]);
|
|
23
|
+
return handleOfflineSubmit;
|
|
24
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Platform } from "react-native";
|
|
2
|
+
import { useScreenInformation } from "./useScreenInformation";
|
|
3
|
+
import { tokens } from "../../utils/design";
|
|
4
|
+
export function useSaveButtonPosition({ formContentHeight, isBottomSheetOpen, showStickySaveButton, keyboardHeight, keyboardScreenY, }) {
|
|
5
|
+
const { headerHeight } = useScreenInformation();
|
|
6
|
+
if (showStickySaveButton) {
|
|
7
|
+
return {
|
|
8
|
+
saveButtonPosition: "sticky",
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
if (Platform.OS === "android" || isBottomSheetOpen) {
|
|
12
|
+
return {
|
|
13
|
+
saveButtonPosition: "inline",
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
const isKeyboardOpen = Boolean(keyboardHeight);
|
|
17
|
+
// tokens["space-large"] acts like a safe space below the form to avoid
|
|
18
|
+
// rendering the sticky button above the form elements.
|
|
19
|
+
const calculatedFormContentHeight = formContentHeight + headerHeight + tokens["space-large"];
|
|
20
|
+
const isKeyboardOverForm = calculatedFormContentHeight > keyboardScreenY;
|
|
21
|
+
const showInlineSaveButton = isKeyboardOpen && isKeyboardOverForm;
|
|
22
|
+
return {
|
|
23
|
+
saveButtonPosition: showInlineSaveButton ? "inline" : "sticky",
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { useWindowDimensions } from "react-native";
|
|
2
|
+
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
3
|
+
import { KEYBOARD_TOP_PADDING_AUTO_SCROLL } from "../constants";
|
|
4
|
+
import { useAtlantisFormContext } from "../context";
|
|
5
|
+
export function useScreenInformation() {
|
|
6
|
+
const { headerHeight } = useAtlantisFormContext();
|
|
7
|
+
const windowHeight = useWindowDimensions().height;
|
|
8
|
+
const headerHeightWithPadding = headerHeight + KEYBOARD_TOP_PADDING_AUTO_SCROLL;
|
|
9
|
+
const insets = useSafeAreaInsets();
|
|
10
|
+
return {
|
|
11
|
+
windowHeight,
|
|
12
|
+
headerHeight: headerHeightWithPadding,
|
|
13
|
+
insets,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./useScrollToError";
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { useCallback, useState } from "react";
|
|
2
|
+
import { Keyboard, Platform, } from "react-native";
|
|
3
|
+
import { useIsScreenReaderEnabled } from "../../../hooks/useIsScreenReaderEnabled";
|
|
4
|
+
import { useErrorMessageContext } from "../../../ErrorMessageWrapper";
|
|
5
|
+
export function useScrollToError({ formState: { errors, isValid, submitCount }, refNode, setFocus, scrollToPosition, }) {
|
|
6
|
+
const [submitCounter, setSubmitCounter] = useState(submitCount);
|
|
7
|
+
const isScreenReaderEnabled = useIsScreenReaderEnabled();
|
|
8
|
+
const manuallyScrollToElement = useManuallyScrollToElement(handleScroll, refNode);
|
|
9
|
+
// Determine if the form has been submitted by checking if the submit count
|
|
10
|
+
// went up.
|
|
11
|
+
const hasBeenSubmitted = submitCounter < submitCount;
|
|
12
|
+
if (!hasBeenSubmitted)
|
|
13
|
+
return;
|
|
14
|
+
if (isScreenReaderEnabled) {
|
|
15
|
+
manuallyScrollToElement();
|
|
16
|
+
Keyboard.dismiss();
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
defaultAutoScroll();
|
|
20
|
+
}
|
|
21
|
+
setSubmitCounter(submitCount);
|
|
22
|
+
function defaultAutoScroll() {
|
|
23
|
+
if (isValid)
|
|
24
|
+
return;
|
|
25
|
+
try {
|
|
26
|
+
focusInputWithRHF(errors, setFocus);
|
|
27
|
+
}
|
|
28
|
+
catch (_a) {
|
|
29
|
+
manuallyScrollToElement();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function handleScroll(...[x, y]) {
|
|
33
|
+
/**
|
|
34
|
+
* Disable scroll animation on android when screen reader is active since it
|
|
35
|
+
* can't accessibility-focus on an offscreen component.
|
|
36
|
+
*/
|
|
37
|
+
const isAndroidWithScreenReader = isScreenReaderEnabled && Platform.OS === "android";
|
|
38
|
+
const shouldAnimateScroll = !isAndroidWithScreenReader;
|
|
39
|
+
scrollToPosition === null || scrollToPosition === void 0 ? void 0 : scrollToPosition(x, y, shouldAnimateScroll);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function focusInputWithRHF(errors, setFocus) {
|
|
43
|
+
const errorMessages = Object.keys(errors);
|
|
44
|
+
setFocus(errorMessages[0]);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Manually scroll to the element by checking which elements has an error from
|
|
48
|
+
* the Error Message Context
|
|
49
|
+
*/
|
|
50
|
+
function useManuallyScrollToElement(callback, refNode) {
|
|
51
|
+
const { elements } = useErrorMessageContext();
|
|
52
|
+
return useCallback(() => {
|
|
53
|
+
const elementWithError = Object.keys(elements).find(el => elements[el].hasErrorMessage);
|
|
54
|
+
if (elementWithError) {
|
|
55
|
+
const element = elements[elementWithError];
|
|
56
|
+
refNode && element.measure(refNode, callback, handleError);
|
|
57
|
+
element.accessibilityFocus();
|
|
58
|
+
}
|
|
59
|
+
}, [callback, elements, refNode]);
|
|
60
|
+
}
|
|
61
|
+
function handleError() {
|
|
62
|
+
return Error("Couldn't scroll to error");
|
|
63
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { defineMessages } from "react-intl";
|
|
2
|
+
export const messages = defineMessages({
|
|
3
|
+
loadingA11YLabel: {
|
|
4
|
+
id: "loadingA11yLabel",
|
|
5
|
+
defaultMessage: "Loading",
|
|
6
|
+
description: "Accessibility label for the loading indicator",
|
|
7
|
+
},
|
|
8
|
+
dismissAlertButton: {
|
|
9
|
+
id: "dismiss",
|
|
10
|
+
defaultMessage: "Dismiss",
|
|
11
|
+
description: "The label for the button to dismiss the alert ",
|
|
12
|
+
},
|
|
13
|
+
retryAlertButton: {
|
|
14
|
+
id: "retry",
|
|
15
|
+
defaultMessage: "Try Again",
|
|
16
|
+
description: "The label for the alert button to try action again",
|
|
17
|
+
},
|
|
18
|
+
unavailableNetworkTitle: {
|
|
19
|
+
id: "unavailableNetworkTitle",
|
|
20
|
+
defaultMessage: "Network unavailable",
|
|
21
|
+
description: "The title for alert about network unavailable",
|
|
22
|
+
},
|
|
23
|
+
unavailableNetworkMessage: {
|
|
24
|
+
id: "unavailableNetworkMessage",
|
|
25
|
+
defaultMessage: "Check your internet connection and try again later.",
|
|
26
|
+
description: "The message for alert about network unavailable",
|
|
27
|
+
},
|
|
28
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export var FormSubmitErrorType;
|
|
2
|
+
(function (FormSubmitErrorType) {
|
|
3
|
+
FormSubmitErrorType["NetworkError"] = "NetworkError";
|
|
4
|
+
FormSubmitErrorType["UserError"] = "UserErrors";
|
|
5
|
+
})(FormSubmitErrorType || (FormSubmitErrorType = {}));
|
|
6
|
+
export var FormBannerMessageType;
|
|
7
|
+
(function (FormBannerMessageType) {
|
|
8
|
+
FormBannerMessageType["WarningMessage"] = "WarningMessage";
|
|
9
|
+
FormBannerMessageType["NoticeMessage"] = "NoticeMessage";
|
|
10
|
+
})(FormBannerMessageType || (FormBannerMessageType = {}));
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import React, { useMemo, useState } from "react";
|
|
2
|
+
import DateTimePicker from "react-native-modal-datetime-picker";
|
|
3
|
+
import { Platform } from "react-native";
|
|
4
|
+
import { useIntl } from "react-intl";
|
|
5
|
+
import { utcToZonedTime } from "date-fns-tz";
|
|
6
|
+
import { format as formatDate } from "date-fns";
|
|
7
|
+
import { messages } from "./messages";
|
|
8
|
+
import { FormField } from "../FormField";
|
|
9
|
+
import { InputPressable } from "../InputPressable";
|
|
10
|
+
import { useAtlantisContext } from "../AtlantisContext";
|
|
11
|
+
function formatInvalidState(error, invalid) {
|
|
12
|
+
if (invalid)
|
|
13
|
+
return invalid;
|
|
14
|
+
if (error && error.message) {
|
|
15
|
+
return error.message;
|
|
16
|
+
}
|
|
17
|
+
return Boolean(error);
|
|
18
|
+
}
|
|
19
|
+
const display = Platform.OS === "ios" ? "inline" : "default";
|
|
20
|
+
/**
|
|
21
|
+
* Allow users to select a date using the device date picker.
|
|
22
|
+
*/
|
|
23
|
+
export function InputDate(props) {
|
|
24
|
+
if (props.name) {
|
|
25
|
+
return (React.createElement(FormField, { name: props.name, defaultValue: props.defaultValue, validations: props.validations }, ({ value, onChange, onBlur }, error) => (React.createElement(InternalInputDate, Object.assign({}, props, { value: value, onChange: (newValue) => {
|
|
26
|
+
var _a;
|
|
27
|
+
onChange(newValue);
|
|
28
|
+
onBlur();
|
|
29
|
+
(_a = props.onChange) === null || _a === void 0 ? void 0 : _a.call(props, newValue);
|
|
30
|
+
}, invalid: formatInvalidState(error, props.invalid) })))));
|
|
31
|
+
}
|
|
32
|
+
return React.createElement(InternalInputDate, Object.assign({}, props));
|
|
33
|
+
}
|
|
34
|
+
function InternalInputDate({ clearable = "always", disabled, emptyValueLabel, invalid, maxDate, minDate, placeholder, value, name, onChange, accessibilityLabel, accessibilityHint, }) {
|
|
35
|
+
const [showPicker, setShowPicker] = useState(false);
|
|
36
|
+
const { formatMessage } = useIntl();
|
|
37
|
+
const { timeZone, dateFormat } = useAtlantisContext();
|
|
38
|
+
const date = useMemo(() => {
|
|
39
|
+
if (typeof value === "string")
|
|
40
|
+
return new Date(value);
|
|
41
|
+
return value;
|
|
42
|
+
}, [value]);
|
|
43
|
+
const formattedDate = useMemo(() => {
|
|
44
|
+
if (date) {
|
|
45
|
+
const zonedTime = utcToZonedTime(date, timeZone);
|
|
46
|
+
return formatDate(zonedTime, dateFormat);
|
|
47
|
+
}
|
|
48
|
+
return emptyValueLabel;
|
|
49
|
+
}, [date, emptyValueLabel, timeZone, dateFormat]);
|
|
50
|
+
const canClearDate = formattedDate === emptyValueLabel ? "never" : clearable;
|
|
51
|
+
const placeholderLabel = placeholder !== null && placeholder !== void 0 ? placeholder : formatMessage(messages.datePlaceholder);
|
|
52
|
+
return (React.createElement(React.Fragment, null,
|
|
53
|
+
React.createElement(InputPressable, { clearable: canClearDate, disabled: disabled, invalid: invalid, placeholder: placeholderLabel, prefix: { icon: "calendar" }, value: formattedDate, onClear: handleClear, onPress: showDatePicker, accessibilityLabel: accessibilityLabel, accessibilityHint: accessibilityHint }),
|
|
54
|
+
React.createElement(DateTimePicker, { testID: "inputDate-datePicker", date: date || undefined, display: display, isVisible: showPicker, maximumDate: maxDate, minimumDate: minDate, mode: "date", onCancel: handleCancel, onConfirm: handleConfirm })));
|
|
55
|
+
function showDatePicker() {
|
|
56
|
+
setShowPicker(true);
|
|
57
|
+
}
|
|
58
|
+
function handleConfirm(newVal) {
|
|
59
|
+
setShowPicker(false);
|
|
60
|
+
onChange === null || onChange === void 0 ? void 0 : onChange(newVal);
|
|
61
|
+
}
|
|
62
|
+
function handleCancel() {
|
|
63
|
+
setShowPicker(false);
|
|
64
|
+
// Ensure a change happens so we trigger the validation of one exists
|
|
65
|
+
onChange === null || onChange === void 0 ? void 0 : onChange(date);
|
|
66
|
+
}
|
|
67
|
+
function handleClear() {
|
|
68
|
+
// Returns null only for Form controlled scenarios due to a limitation of react-hook-form that doesn't allow passing undefined to form values.
|
|
69
|
+
if (name) {
|
|
70
|
+
onChange === null || onChange === void 0 ? void 0 : onChange(null);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
onChange === null || onChange === void 0 ? void 0 : onChange(undefined);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { InputDate } from "./InputDate";
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import React, { useCallback, useRef, useState } from "react";
|
|
2
|
+
import { Pressable, View, useWindowDimensions, } from "react-native";
|
|
3
|
+
import { Portal } from "react-native-portalize";
|
|
4
|
+
import { useIntl } from "react-intl";
|
|
5
|
+
import { useSafeAreaFrame } from "react-native-safe-area-context";
|
|
6
|
+
import { styles } from "./Menu.style";
|
|
7
|
+
import { messages } from "./messages";
|
|
8
|
+
import { findViewpoint } from "./utils";
|
|
9
|
+
import { MenuOption } from "./components/MenuOption";
|
|
10
|
+
import { Overlay } from "./components/Overlay";
|
|
11
|
+
import { tokens } from "../utils/design";
|
|
12
|
+
import { Button } from "../Button";
|
|
13
|
+
import { Content } from "../Content";
|
|
14
|
+
import { useAtlantisContext } from "../AtlantisContext";
|
|
15
|
+
export function Menu({ menuOptions, customActivator }) {
|
|
16
|
+
const [open, setOpen] = useState(false);
|
|
17
|
+
const [menuPosition, setMenuPosition] = useState();
|
|
18
|
+
const activatorLayout = useRef();
|
|
19
|
+
const menuButtonRef = useRef();
|
|
20
|
+
const screenInfo = useScreenInformation();
|
|
21
|
+
const { formatMessage } = useIntl();
|
|
22
|
+
const findMenuLayout = useCallback(() => {
|
|
23
|
+
if (activatorLayout.current) {
|
|
24
|
+
setMenuPosition(findViewpoint(screenInfo, activatorLayout.current));
|
|
25
|
+
}
|
|
26
|
+
}, [screenInfo, activatorLayout]);
|
|
27
|
+
const activatorOnPress = (onPress) => {
|
|
28
|
+
var _a;
|
|
29
|
+
(_a = menuButtonRef.current) === null || _a === void 0 ? void 0 : _a.measureInWindow((x, y, width, height) => {
|
|
30
|
+
activatorLayout.current = {
|
|
31
|
+
x,
|
|
32
|
+
y,
|
|
33
|
+
width,
|
|
34
|
+
height,
|
|
35
|
+
};
|
|
36
|
+
findMenuLayout();
|
|
37
|
+
setOpen(true);
|
|
38
|
+
onPress && onPress();
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
return (React.createElement(React.Fragment, null,
|
|
42
|
+
React.createElement(View, { ref: ref => {
|
|
43
|
+
menuButtonRef.current = ref;
|
|
44
|
+
}, collapsable: false },
|
|
45
|
+
customActivator && (React.createElement(Pressable, { style: ({ pressed }) => [
|
|
46
|
+
{
|
|
47
|
+
opacity: pressed ? tokens["opacity-pressed"] : 1,
|
|
48
|
+
},
|
|
49
|
+
], pointerEvents: "box-only", onPress: () => {
|
|
50
|
+
activatorOnPress(customActivator.props.onPress);
|
|
51
|
+
}, onLongPress: customActivator.props.onLongPress }, customActivator)),
|
|
52
|
+
!customActivator && (React.createElement(Button, { icon: "more", accessibilityLabel: formatMessage(messages.more), variation: "cancel", type: "tertiary", onPress: () => {
|
|
53
|
+
activatorOnPress();
|
|
54
|
+
} }))),
|
|
55
|
+
React.createElement(Portal, null, open && (React.createElement(React.Fragment, null,
|
|
56
|
+
React.createElement(Overlay, { setOpen: setOpen }),
|
|
57
|
+
React.createElement(View, { style: [open && menuPosition, styles.menu] },
|
|
58
|
+
React.createElement(Content, { spacing: "none", childSpacing: "small" }, menuOptions === null || menuOptions === void 0 ? void 0 : menuOptions.map((menuOption, index) => {
|
|
59
|
+
return (React.createElement(MenuOption, Object.assign({}, menuOption, { key: index, setOpen: setOpen })));
|
|
60
|
+
}))))))));
|
|
61
|
+
}
|
|
62
|
+
function useScreenInformation() {
|
|
63
|
+
const { headerHeight } = useAtlantisContext();
|
|
64
|
+
const windowWidth = useWindowDimensions().width;
|
|
65
|
+
const { height: windowHeight } = useSafeAreaFrame();
|
|
66
|
+
return { headerHeight, windowWidth, windowHeight };
|
|
67
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { StyleSheet } from "react-native";
|
|
2
|
+
import { tokens } from "../utils/design";
|
|
3
|
+
const menuWidth = 208;
|
|
4
|
+
export const styles = StyleSheet.create({
|
|
5
|
+
menu: Object.assign({ position: "absolute", backgroundColor: tokens["color-surface"], paddingHorizontal: tokens["space-small"], paddingVertical: tokens["space-small"] + tokens["space-smallest"], borderRadius: tokens["radius-larger"], width: menuWidth }, tokens["shadow-high"]),
|
|
6
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Pressable, View } from "react-native";
|
|
3
|
+
import capitalize from "lodash/capitalize";
|
|
4
|
+
import { styles } from "./MenuOption.style";
|
|
5
|
+
import { tokens } from "../../../utils/design";
|
|
6
|
+
import { Flex } from "../../../Flex";
|
|
7
|
+
import { Typography } from "../../../Typography";
|
|
8
|
+
import { Icon } from "../../../Icon";
|
|
9
|
+
export function MenuOption({ label, icon, iconColor = "heading", textAlign, destructive, textTransform = "capitalize", onPress, setOpen, }) {
|
|
10
|
+
const destructiveColor = "critical";
|
|
11
|
+
const textVariation = destructive ? destructiveColor : "heading";
|
|
12
|
+
return (React.createElement(View, { testID: "ATL-MENU-OPTIONS" },
|
|
13
|
+
React.createElement(Pressable, { style: ({ pressed }) => [
|
|
14
|
+
styles.menuOption,
|
|
15
|
+
{ opacity: pressed ? tokens["opacity-pressed"] : 1 },
|
|
16
|
+
], onPress: () => {
|
|
17
|
+
onPress();
|
|
18
|
+
setOpen(false);
|
|
19
|
+
}, accessibilityLabel: label },
|
|
20
|
+
React.createElement(Flex, { template: ["grow", "shrink"], align: "flex-start", gap: "smaller" },
|
|
21
|
+
React.createElement(Typography, { selectable: false, color: textVariation, fontWeight: "semiBold", lineHeight: "large", align: textAlign }, textTransform === "capitalize"
|
|
22
|
+
? capitalize(label.toLocaleLowerCase())
|
|
23
|
+
: label),
|
|
24
|
+
icon && (React.createElement(Icon, { name: icon, color: destructive ? destructiveColor : iconColor }))))));
|
|
25
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { StyleSheet } from "react-native";
|
|
2
|
+
import { tokens } from "../../../utils/design";
|
|
3
|
+
export const styles = StyleSheet.create({
|
|
4
|
+
menuOption: {
|
|
5
|
+
display: "flex",
|
|
6
|
+
paddingHorizontal: tokens["space-base"],
|
|
7
|
+
paddingVertical: tokens["space-small"],
|
|
8
|
+
borderRadius: tokens["radius-large"],
|
|
9
|
+
},
|
|
10
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { MenuOption } from "./MenuOption";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Pressable, View } from "react-native";
|
|
3
|
+
import { styles } from "./Overlay.style";
|
|
4
|
+
export function Overlay({ setOpen }) {
|
|
5
|
+
return (React.createElement(Pressable, { onPressIn: () => {
|
|
6
|
+
setOpen(false);
|
|
7
|
+
} },
|
|
8
|
+
React.createElement(View, { style: styles.overlay })));
|
|
9
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Dimensions, StyleSheet } from "react-native";
|
|
2
|
+
import { tokens } from "../../../utils/design";
|
|
3
|
+
const { height } = Dimensions.get("window");
|
|
4
|
+
export const styles = StyleSheet.create({
|
|
5
|
+
overlay: Object.assign(Object.assign({}, StyleSheet.absoluteFillObject), { backgroundColor: tokens["color-overlay"], opacity: 0, height }),
|
|
6
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Overlay } from "./Overlay";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Menu } from "./Menu";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { styles } from "./Menu.style";
|
|
2
|
+
export function findViewpoint(screenInfo, activatorLayout) {
|
|
3
|
+
const { windowHeight, windowWidth, headerHeight } = screenInfo;
|
|
4
|
+
const pos = {};
|
|
5
|
+
const menuWidth = styles.menu.width;
|
|
6
|
+
const windowHalf = (windowHeight - headerHeight) / 2 + headerHeight;
|
|
7
|
+
const menuPositionVertical = activatorLayout.y + activatorLayout.height > windowHalf
|
|
8
|
+
? "menuAbove"
|
|
9
|
+
: "menuBelow";
|
|
10
|
+
const menuPositionHorizontal = windowWidth / 2 > activatorLayout.width / 2 + activatorLayout.x
|
|
11
|
+
? "menuRight"
|
|
12
|
+
: "menuLeft";
|
|
13
|
+
const menuPadding = 36;
|
|
14
|
+
getVerticalPosition(pos, windowHeight, activatorLayout, menuPositionVertical);
|
|
15
|
+
getHorizontalPosition(pos, activatorLayout, windowWidth, menuPadding, menuWidth, menuPositionHorizontal);
|
|
16
|
+
return pos;
|
|
17
|
+
}
|
|
18
|
+
function getVerticalPosition(pos, windowHeight, activatorLayout, menuPositionVertical) {
|
|
19
|
+
if (menuPositionVertical === "menuAbove") {
|
|
20
|
+
getAbovePosition(pos, windowHeight, activatorLayout);
|
|
21
|
+
}
|
|
22
|
+
if (menuPositionVertical === "menuBelow") {
|
|
23
|
+
getBelowPosition(pos, activatorLayout);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function getBelowPosition(pos, activatorLayout) {
|
|
27
|
+
pos.top = activatorLayout.y + activatorLayout.height;
|
|
28
|
+
}
|
|
29
|
+
function getAbovePosition(pos, windowHeight, activatorLayout) {
|
|
30
|
+
pos.bottom = windowHeight - activatorLayout.y;
|
|
31
|
+
}
|
|
32
|
+
function getHorizontalPosition(pos, activatorLayout, windowWidth, menuPadding, menuWidth, menuPositionHorizontal) {
|
|
33
|
+
if (menuPositionHorizontal === "menuRight") {
|
|
34
|
+
getRightPosition(pos, activatorLayout, windowWidth, menuPadding, menuWidth);
|
|
35
|
+
}
|
|
36
|
+
if (menuPositionHorizontal === "menuLeft") {
|
|
37
|
+
getLeftPosition(pos, activatorLayout, windowWidth, menuPadding, menuWidth);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function getLeftPosition(pos, activatorLayout, windowWidth, menuHorizontalPadding, menuWidth) {
|
|
41
|
+
const overflowLeft = windowWidth -
|
|
42
|
+
activatorLayout.x -
|
|
43
|
+
activatorLayout.width +
|
|
44
|
+
activatorLayout.width / 2 -
|
|
45
|
+
menuHorizontalPadding +
|
|
46
|
+
menuWidth >
|
|
47
|
+
windowWidth;
|
|
48
|
+
const overflowRight = windowWidth -
|
|
49
|
+
activatorLayout.x -
|
|
50
|
+
activatorLayout.width +
|
|
51
|
+
activatorLayout.width / 2 -
|
|
52
|
+
menuHorizontalPadding <
|
|
53
|
+
0;
|
|
54
|
+
if (overflowLeft) {
|
|
55
|
+
pos.right = undefined;
|
|
56
|
+
pos.left = 0;
|
|
57
|
+
}
|
|
58
|
+
else if (overflowRight) {
|
|
59
|
+
pos.right = 0;
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
pos.right =
|
|
63
|
+
windowWidth -
|
|
64
|
+
activatorLayout.x -
|
|
65
|
+
activatorLayout.width +
|
|
66
|
+
activatorLayout.width / 2 -
|
|
67
|
+
menuHorizontalPadding;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function getRightPosition(pos, activatorLayout, windowWidth, menuPadding, menuWidth) {
|
|
71
|
+
const overflowRight = activatorLayout.x + activatorLayout.width / 2 - menuPadding + menuWidth >
|
|
72
|
+
windowWidth;
|
|
73
|
+
const overflowLeft = activatorLayout.x + activatorLayout.width / 2 - menuPadding < 0;
|
|
74
|
+
if (overflowRight) {
|
|
75
|
+
pos.left = undefined;
|
|
76
|
+
pos.right = 0;
|
|
77
|
+
}
|
|
78
|
+
else if (overflowLeft) {
|
|
79
|
+
pos.left = 0;
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
pos.left = activatorLayout.x + activatorLayout.width / 2 - menuPadding;
|
|
83
|
+
}
|
|
84
|
+
}
|
package/dist/src/index.js
CHANGED
|
@@ -16,18 +16,21 @@ export * from "./Divider";
|
|
|
16
16
|
export * from "./EmptyState";
|
|
17
17
|
export * from "./ErrorMessageWrapper";
|
|
18
18
|
export * from "./Flex";
|
|
19
|
+
export * from "./Form";
|
|
19
20
|
export * from "./FormField";
|
|
20
21
|
export * from "./Heading";
|
|
21
22
|
export * from "./Icon";
|
|
22
23
|
export * from "./IconButton";
|
|
23
24
|
export * from "./InputFieldWrapper";
|
|
24
25
|
export * from "./InputCurrency";
|
|
26
|
+
export * from "./InputDate";
|
|
25
27
|
export * from "./InputNumber";
|
|
26
28
|
export * from "./InputPassword";
|
|
27
29
|
export * from "./InputPressable";
|
|
28
30
|
export * from "./InputSearch";
|
|
29
31
|
export * from "./InputTime";
|
|
30
32
|
export * from "./InputText";
|
|
33
|
+
export * from "./Menu";
|
|
31
34
|
export * from "./TextList";
|
|
32
35
|
export * from "./ProgressBar";
|
|
33
36
|
export * from "./Select";
|