@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.
Files changed (184) hide show
  1. package/dist/src/AtlantisContext/AtlantisContext.js +2 -0
  2. package/dist/src/Form/Form.js +187 -0
  3. package/dist/src/Form/Form.style.js +33 -0
  4. package/dist/src/Form/components/FormActionBar/FormActionBar.js +21 -0
  5. package/dist/src/Form/components/FormActionBar/FormActionBar.style.js +5 -0
  6. package/dist/src/Form/components/FormActionBar/index.js +1 -0
  7. package/dist/src/Form/components/FormBody/FormBody.js +20 -0
  8. package/dist/src/Form/components/FormBody/FormBody.style.js +26 -0
  9. package/dist/src/Form/components/FormBody/index.js +1 -0
  10. package/dist/src/Form/components/FormCache/FormCache.js +34 -0
  11. package/dist/src/Form/components/FormErrorBanner/FormErrorBanner.js +21 -0
  12. package/dist/src/Form/components/FormErrorBanner/index.js +1 -0
  13. package/dist/src/Form/components/FormErrorBanner/messages.js +13 -0
  14. package/dist/src/Form/components/FormMask/FormMask.js +11 -0
  15. package/dist/src/Form/components/FormMask/FormMask.style.js +15 -0
  16. package/dist/src/Form/components/FormMask/index.js +1 -0
  17. package/dist/src/Form/components/FormMessage/FormMessage.js +48 -0
  18. package/dist/src/Form/components/FormMessage/components/InternalFormMessage/InternalFormMessage.js +28 -0
  19. package/dist/src/Form/components/FormMessage/components/InternalFormMessage/InternalFormMessage.style.js +17 -0
  20. package/dist/src/Form/components/FormMessage/components/InternalFormMessage/index.js +1 -0
  21. package/dist/src/Form/components/FormMessage/components/InternalFormMessage/messages.js +8 -0
  22. package/dist/src/Form/components/FormMessage/index.js +1 -0
  23. package/dist/src/Form/components/FormMessageBanner/FormMessageBanner.js +15 -0
  24. package/dist/src/Form/components/FormMessageBanner/index.js +1 -0
  25. package/dist/src/Form/components/FormSaveButton/FormSaveButton.js +69 -0
  26. package/dist/src/Form/components/FormSaveButton/index.js +1 -0
  27. package/dist/src/Form/components/FormSaveButton/messages.js +8 -0
  28. package/dist/src/Form/constants.js +2 -0
  29. package/dist/src/Form/context/AtlantisFormContext.js +16 -0
  30. package/dist/src/Form/context/index.js +1 -0
  31. package/dist/src/Form/context/types.js +1 -0
  32. package/dist/src/Form/hooks/useFormViewRefs.js +14 -0
  33. package/dist/src/Form/hooks/useInternalForm.js +37 -0
  34. package/dist/src/Form/hooks/useOfflineHandler.js +24 -0
  35. package/dist/src/Form/hooks/useSaveButtonPosition.js +25 -0
  36. package/dist/src/Form/hooks/useScreenInformation.js +15 -0
  37. package/dist/src/Form/hooks/useScrollToError/index.js +1 -0
  38. package/dist/src/Form/hooks/useScrollToError/useScrollToError.js +63 -0
  39. package/dist/src/Form/index.js +4 -0
  40. package/dist/src/Form/messages.js +28 -0
  41. package/dist/src/Form/types.js +10 -0
  42. package/dist/src/InputDate/InputDate.js +76 -0
  43. package/dist/src/InputDate/index.js +1 -0
  44. package/dist/src/InputDate/messages.js +8 -0
  45. package/dist/src/Menu/Menu.js +67 -0
  46. package/dist/src/Menu/Menu.style.js +6 -0
  47. package/dist/src/Menu/components/MenuOption/MenuOption.js +25 -0
  48. package/dist/src/Menu/components/MenuOption/MenuOption.style.js +10 -0
  49. package/dist/src/Menu/components/MenuOption/index.js +1 -0
  50. package/dist/src/Menu/components/Overlay/Overlay.js +9 -0
  51. package/dist/src/Menu/components/Overlay/Overlay.style.js +6 -0
  52. package/dist/src/Menu/components/Overlay/index.js +1 -0
  53. package/dist/src/Menu/index.js +1 -0
  54. package/dist/src/Menu/messages.js +8 -0
  55. package/dist/src/Menu/types.js +1 -0
  56. package/dist/src/Menu/utils.js +84 -0
  57. package/dist/src/index.js +3 -0
  58. package/dist/tsconfig.tsbuildinfo +1 -1
  59. package/dist/types/src/AtlantisContext/AtlantisContext.d.ts +7 -1
  60. package/dist/types/src/Form/Form.d.ts +4 -0
  61. package/dist/types/src/Form/Form.style.d.ts +31 -0
  62. package/dist/types/src/Form/components/FormActionBar/FormActionBar.d.ts +13 -0
  63. package/dist/types/src/Form/components/FormActionBar/FormActionBar.style.d.ts +15 -0
  64. package/dist/types/src/Form/components/FormActionBar/index.d.ts +2 -0
  65. package/dist/types/src/Form/components/FormBody/FormBody.d.ts +10 -0
  66. package/dist/types/src/Form/components/FormBody/FormBody.style.d.ts +24 -0
  67. package/dist/types/src/Form/components/FormBody/index.d.ts +1 -0
  68. package/dist/types/src/Form/components/FormCache/FormCache.d.ts +10 -0
  69. package/dist/types/src/Form/components/FormErrorBanner/FormErrorBanner.d.ts +3 -0
  70. package/dist/types/src/Form/components/FormErrorBanner/index.d.ts +1 -0
  71. package/dist/types/src/Form/components/FormErrorBanner/messages.d.ts +12 -0
  72. package/dist/types/src/Form/components/FormMask/FormMask.d.ts +2 -0
  73. package/dist/types/src/Form/components/FormMask/FormMask.style.d.ts +13 -0
  74. package/dist/types/src/Form/components/FormMask/index.d.ts +1 -0
  75. package/dist/types/src/Form/components/FormMessage/FormMessage.d.ts +19 -0
  76. package/dist/types/src/Form/components/FormMessage/components/InternalFormMessage/InternalFormMessage.d.ts +8 -0
  77. package/dist/types/src/Form/components/FormMessage/components/InternalFormMessage/InternalFormMessage.style.d.ts +20 -0
  78. package/dist/types/src/Form/components/FormMessage/components/InternalFormMessage/index.d.ts +1 -0
  79. package/dist/types/src/Form/components/FormMessage/components/InternalFormMessage/messages.d.ts +7 -0
  80. package/dist/types/src/Form/components/FormMessage/index.d.ts +1 -0
  81. package/dist/types/src/Form/components/FormMessageBanner/FormMessageBanner.d.ts +7 -0
  82. package/dist/types/src/Form/components/FormMessageBanner/index.d.ts +1 -0
  83. package/dist/types/src/Form/components/FormSaveButton/FormSaveButton.d.ts +3 -0
  84. package/dist/types/src/Form/components/FormSaveButton/index.d.ts +1 -0
  85. package/dist/types/src/Form/components/FormSaveButton/messages.d.ts +7 -0
  86. package/dist/types/src/Form/constants.d.ts +2 -0
  87. package/dist/types/src/Form/context/AtlantisFormContext.d.ts +12 -0
  88. package/dist/types/src/Form/context/index.d.ts +2 -0
  89. package/dist/types/src/Form/context/types.d.ts +26 -0
  90. package/dist/types/src/Form/hooks/useFormViewRefs.d.ts +10 -0
  91. package/dist/types/src/Form/hooks/useInternalForm.d.ts +19 -0
  92. package/dist/types/src/Form/hooks/useOfflineHandler.d.ts +1 -0
  93. package/dist/types/src/Form/hooks/useSaveButtonPosition.d.ts +12 -0
  94. package/dist/types/src/Form/hooks/useScreenInformation.d.ts +8 -0
  95. package/dist/types/src/Form/hooks/useScrollToError/index.d.ts +1 -0
  96. package/dist/types/src/Form/hooks/useScrollToError/useScrollToError.d.ts +10 -0
  97. package/dist/types/src/Form/index.d.ts +5 -0
  98. package/dist/types/src/Form/messages.d.ts +27 -0
  99. package/dist/types/src/Form/types.d.ts +199 -0
  100. package/dist/types/src/InputDate/InputDate.d.ts +74 -0
  101. package/dist/types/src/InputDate/index.d.ts +1 -0
  102. package/dist/types/src/InputDate/messages.d.ts +7 -0
  103. package/dist/types/src/InputNumber/InputNumber.d.ts +1 -1
  104. package/dist/types/src/Menu/Menu.d.ts +3 -0
  105. package/dist/types/src/Menu/Menu.style.d.ts +18 -0
  106. package/dist/types/src/Menu/components/MenuOption/MenuOption.d.ts +3 -0
  107. package/dist/types/src/Menu/components/MenuOption/MenuOption.style.d.ts +8 -0
  108. package/dist/types/src/Menu/components/MenuOption/index.d.ts +1 -0
  109. package/dist/types/src/Menu/components/Overlay/Overlay.d.ts +3 -0
  110. package/dist/types/src/Menu/components/Overlay/Overlay.style.d.ts +12 -0
  111. package/dist/types/src/Menu/components/Overlay/index.d.ts +1 -0
  112. package/dist/types/src/Menu/index.d.ts +2 -0
  113. package/dist/types/src/Menu/messages.d.ts +7 -0
  114. package/dist/types/src/Menu/types.d.ts +22 -0
  115. package/dist/types/src/Menu/utils.d.ts +10 -0
  116. package/dist/types/src/index.d.ts +3 -0
  117. package/package.json +3 -2
  118. package/src/AtlantisContext/AtlantisContext.tsx +10 -1
  119. package/src/Form/Form.style.ts +34 -0
  120. package/src/Form/Form.test.tsx +588 -0
  121. package/src/Form/Form.tsx +296 -0
  122. package/src/Form/components/FormActionBar/FormActionBar.style.ts +11 -0
  123. package/src/Form/components/FormActionBar/FormActionBar.tsx +63 -0
  124. package/src/Form/components/FormActionBar/index.ts +2 -0
  125. package/src/Form/components/FormBody/FormBody.style.ts +27 -0
  126. package/src/Form/components/FormBody/FormBody.tsx +62 -0
  127. package/src/Form/components/FormBody/index.ts +1 -0
  128. package/src/Form/components/FormCache/FormCache.tsx +50 -0
  129. package/src/Form/components/FormErrorBanner/FormErrorBanner.test.tsx +124 -0
  130. package/src/Form/components/FormErrorBanner/FormErrorBanner.tsx +34 -0
  131. package/src/Form/components/FormErrorBanner/index.ts +1 -0
  132. package/src/Form/components/FormErrorBanner/messages.ts +14 -0
  133. package/src/Form/components/FormMask/FormMask.style.tsx +16 -0
  134. package/src/Form/components/FormMask/FormMask.tsx +19 -0
  135. package/src/Form/components/FormMask/index.ts +1 -0
  136. package/src/Form/components/FormMessage/FormMessage.test.tsx +72 -0
  137. package/src/Form/components/FormMessage/FormMessage.tsx +63 -0
  138. package/src/Form/components/FormMessage/components/InternalFormMessage/InternalFormMessage.style.ts +18 -0
  139. package/src/Form/components/FormMessage/components/InternalFormMessage/InternalFormMessage.tsx +55 -0
  140. package/src/Form/components/FormMessage/components/InternalFormMessage/index.ts +1 -0
  141. package/src/Form/components/FormMessage/components/InternalFormMessage/messages.ts +10 -0
  142. package/src/Form/components/FormMessage/index.ts +1 -0
  143. package/src/Form/components/FormMessageBanner/FormMessageBanner.test.tsx +27 -0
  144. package/src/Form/components/FormMessageBanner/FormMessageBanner.tsx +33 -0
  145. package/src/Form/components/FormMessageBanner/index.ts +1 -0
  146. package/src/Form/components/FormSaveButton/FormSaveButton.test.tsx +159 -0
  147. package/src/Form/components/FormSaveButton/FormSaveButton.tsx +103 -0
  148. package/src/Form/components/FormSaveButton/index.ts +1 -0
  149. package/src/Form/components/FormSaveButton/messages.ts +9 -0
  150. package/src/Form/constants.ts +2 -0
  151. package/src/Form/context/AtlantisFormContext.test.tsx +45 -0
  152. package/src/Form/context/AtlantisFormContext.tsx +21 -0
  153. package/src/Form/context/index.ts +5 -0
  154. package/src/Form/context/types.ts +34 -0
  155. package/src/Form/hooks/useFormViewRefs.ts +23 -0
  156. package/src/Form/hooks/useInternalForm.ts +99 -0
  157. package/src/Form/hooks/useOfflineHandler.ts +36 -0
  158. package/src/Form/hooks/useSaveButtonPosition.ts +52 -0
  159. package/src/Form/hooks/useScreenInformation.ts +25 -0
  160. package/src/Form/hooks/useScrollToError/index.ts +1 -0
  161. package/src/Form/hooks/useScrollToError/useScrollToError.test.tsx +103 -0
  162. package/src/Form/hooks/useScrollToError/useScrollToError.ts +102 -0
  163. package/src/Form/index.ts +13 -0
  164. package/src/Form/messages.ts +33 -0
  165. package/src/Form/types.ts +255 -0
  166. package/src/InputDate/InputDate.test.tsx +295 -0
  167. package/src/InputDate/InputDate.tsx +231 -0
  168. package/src/InputDate/index.ts +1 -0
  169. package/src/InputDate/messages.ts +9 -0
  170. package/src/InputNumber/InputNumber.tsx +1 -1
  171. package/src/Menu/Menu.style.ts +16 -0
  172. package/src/Menu/Menu.test.tsx +201 -0
  173. package/src/Menu/Menu.tsx +116 -0
  174. package/src/Menu/components/MenuOption/MenuOption.style.tsx +11 -0
  175. package/src/Menu/components/MenuOption/MenuOption.tsx +63 -0
  176. package/src/Menu/components/MenuOption/index.ts +1 -0
  177. package/src/Menu/components/Overlay/Overlay.style.ts +13 -0
  178. package/src/Menu/components/Overlay/Overlay.tsx +16 -0
  179. package/src/Menu/components/Overlay/index.ts +1 -0
  180. package/src/Menu/index.ts +6 -0
  181. package/src/Menu/messages.ts +9 -0
  182. package/src/Menu/types.ts +25 -0
  183. package/src/Menu/utils.ts +151 -0
  184. package/src/index.ts +3 -0
@@ -0,0 +1,16 @@
1
+ import { StyleSheet } from "react-native";
2
+ import { tokens } from "../../../utils/design";
3
+
4
+ export const styles = StyleSheet.create({
5
+ mask: {
6
+ zIndex: tokens["elevation-modal"],
7
+ width: "100%",
8
+ height: "100%",
9
+ position: "absolute",
10
+ top: 0,
11
+ left: 0,
12
+ backgroundColor: tokens["color-overlay--dimmed"],
13
+ padding: tokens["space-base"],
14
+ justifyContent: "center",
15
+ },
16
+ });
@@ -0,0 +1,19 @@
1
+ import React from "react";
2
+ import { View } from "react-native";
3
+ import { useIntl } from "react-intl";
4
+ import { styles } from "./FormMask.style";
5
+ import { ActivityIndicator } from "../../../ActivityIndicator";
6
+ import { messages } from "../../messages";
7
+
8
+ export function FormMask(): JSX.Element {
9
+ const { formatMessage } = useIntl();
10
+
11
+ return (
12
+ <View
13
+ style={styles.mask}
14
+ accessibilityLabel={formatMessage(messages.loadingA11YLabel)}
15
+ >
16
+ <ActivityIndicator />
17
+ </View>
18
+ );
19
+ }
@@ -0,0 +1 @@
1
+ export { FormMask } from "./FormMask";
@@ -0,0 +1,72 @@
1
+ import React from "react";
2
+ import { fireEvent, render } from "@testing-library/react-native";
3
+ import { FormMessage } from ".";
4
+
5
+ describe("FormMessage", () => {
6
+ it("should render null when there are no message to show", () => {
7
+ const view = render(<FormMessage />);
8
+ expect(view.toJSON()).toMatchInlineSnapshot(`null`);
9
+ });
10
+
11
+ it("should show the message", () => {
12
+ const { getByText } = render(<FormMessage />);
13
+
14
+ const description = "🔥";
15
+ FormMessage.show({ description });
16
+ expect(getByText(description)).toBeDefined();
17
+ });
18
+
19
+ it("should close the message", () => {
20
+ const { getByText, queryByText } = render(<FormMessage />);
21
+
22
+ const description = "🌚";
23
+ FormMessage.show({ description });
24
+ expect(getByText(description)).toBeDefined();
25
+
26
+ FormMessage.close();
27
+ expect(queryByText(description)).toBeNull();
28
+ });
29
+
30
+ describe("Opening another message through a message", () => {
31
+ const firstMessage = "I am the first message";
32
+ const secondMessage = "Second message here";
33
+
34
+ const showMessage = () =>
35
+ FormMessage.show({
36
+ description: firstMessage,
37
+ primaryAction: {
38
+ label: "Click me",
39
+ onPress: () => FormMessage.show({ description: secondMessage }),
40
+ },
41
+ });
42
+
43
+ it("should show the most recent message", () => {
44
+ const { getByText, queryByText, getByLabelText } = render(
45
+ <FormMessage />,
46
+ );
47
+
48
+ showMessage();
49
+
50
+ expect(getByText(firstMessage)).toBeDefined();
51
+ expect(queryByText(secondMessage)).toBeNull();
52
+
53
+ fireEvent.press(getByLabelText("Click me"));
54
+
55
+ expect(getByText(secondMessage)).toBeDefined();
56
+ expect(queryByText(firstMessage)).toBeNull();
57
+ });
58
+
59
+ it("should close the most recent message", () => {
60
+ const { getByText, queryByText, getByLabelText } = render(
61
+ <FormMessage />,
62
+ );
63
+
64
+ showMessage();
65
+ fireEvent.press(getByLabelText("Click me"));
66
+ FormMessage.close();
67
+
68
+ expect(getByText(firstMessage)).toBeDefined();
69
+ expect(queryByText(secondMessage)).toBeNull();
70
+ });
71
+ });
72
+ });
@@ -0,0 +1,63 @@
1
+ import React, { useCallback, useState } from "react";
2
+ import { InternalFormMessage } from "./components/InternalFormMessage";
3
+ import { EmptyStateProps } from "../../../EmptyState";
4
+
5
+ type FormMessageData = EmptyStateProps;
6
+
7
+ let open: ((messageData: FormMessageData) => void) | undefined;
8
+ let close: (() => void) | undefined;
9
+
10
+ /**
11
+ * Show a message that takes over the whole screen to the user. This provides a
12
+ * more urgent feedback when the user does an action that requires attention
13
+ * their full attention.
14
+ *
15
+ * By default, rendering `<FormMessage />` on a screen won't show any messages
16
+ * because it's only a container. Use `FormMessage.open(...)`. to show a
17
+ * message to the user. Use `FormMessage.close()` to close the most
18
+ * recent message.
19
+ */
20
+ export const FormMessage = (): JSX.Element => {
21
+ const [data, setData] = useState<FormMessageData[]>([]);
22
+
23
+ open = useCallback(
24
+ (messageData: FormMessageData) => {
25
+ setData([...data, messageData]);
26
+ },
27
+ [data],
28
+ );
29
+
30
+ close = useCallback(() => {
31
+ const newValue = data.slice(0, -1);
32
+ setData(newValue);
33
+ }, [data]);
34
+
35
+ if (data.length === 0) {
36
+ return <></>;
37
+ }
38
+
39
+ const lastMessage = data[data.length - 1];
40
+ return <InternalFormMessage data={lastMessage} onRequestClose={close} />;
41
+ };
42
+
43
+ FormMessage.show = (messageData: FormMessageData) => {
44
+ if (open) {
45
+ open(messageData);
46
+ } else {
47
+ warnOnUndefinedFunctions("show");
48
+ }
49
+ };
50
+
51
+ FormMessage.close = () => {
52
+ if (close) {
53
+ close();
54
+ } else {
55
+ warnOnUndefinedFunctions("close");
56
+ }
57
+ };
58
+
59
+ function warnOnUndefinedFunctions(method: string) {
60
+ console.warn(
61
+ `Could not ${method} "FormMessage". Either you're calling this method before the component mounts or you're using this without the "<Form />" component. If you're using "FormMessage" without the "Form", include "<FormMessage />" on your component to start using it.`,
62
+ );
63
+ }
@@ -0,0 +1,18 @@
1
+ import { StyleSheet } from "react-native";
2
+ import { tokens } from "../../../../../utils/design";
3
+
4
+ export const styles = StyleSheet.create({
5
+ wrapper: {
6
+ backgroundColor: tokens["color-surface"],
7
+ flex: 1,
8
+ justifyContent: "center",
9
+ },
10
+ closeAction: {
11
+ position: "absolute",
12
+ top: 0,
13
+ right: 0,
14
+ zIndex: 1,
15
+ },
16
+ scrollWrapper: { height: "100%" },
17
+ scrollWrapperContent: { flexGrow: 1, justifyContent: "center" },
18
+ });
@@ -0,0 +1,55 @@
1
+ import React, { useMemo } from "react";
2
+ import { SafeAreaView } from "react-native-safe-area-context";
3
+ import { Modal, StatusBar, View } from "react-native";
4
+ import { ScrollView } from "react-native-gesture-handler";
5
+ import { useIntl } from "react-intl";
6
+ import { styles } from "./InternalFormMessage.style";
7
+ import { messages } from "./messages";
8
+ import { EmptyState, EmptyStateProps } from "../../../../../EmptyState";
9
+
10
+ interface FormMessageProps {
11
+ readonly data: EmptyStateProps;
12
+ readonly onRequestClose: () => void;
13
+ }
14
+
15
+ export function InternalFormMessage({
16
+ data,
17
+ onRequestClose,
18
+ }: FormMessageProps): JSX.Element {
19
+ const { formatMessage } = useIntl();
20
+ const emptyStateData: EmptyStateProps = useMemo(() => {
21
+ if (data.secondaryAction) {
22
+ return data;
23
+ } else {
24
+ return {
25
+ ...data,
26
+ secondaryAction: {
27
+ label: formatMessage(messages.goBackButton),
28
+ onPress: onRequestClose,
29
+ },
30
+ };
31
+ }
32
+ }, [data, formatMessage, onRequestClose]);
33
+
34
+ return (
35
+ <Modal
36
+ animationType="fade"
37
+ transparent={true}
38
+ visible={true}
39
+ onRequestClose={onRequestClose}
40
+ >
41
+ <View style={styles.wrapper}>
42
+ <StatusBar barStyle="dark-content" />
43
+ <SafeAreaView>
44
+ <ScrollView
45
+ style={styles.scrollWrapper}
46
+ contentContainerStyle={styles.scrollWrapperContent}
47
+ centerContent={true}
48
+ >
49
+ <EmptyState {...emptyStateData} />
50
+ </ScrollView>
51
+ </SafeAreaView>
52
+ </View>
53
+ </Modal>
54
+ );
55
+ }
@@ -0,0 +1 @@
1
+ export { InternalFormMessage } from "./InternalFormMessage";
@@ -0,0 +1,10 @@
1
+ import { defineMessages } from "react-intl";
2
+
3
+ export const messages = defineMessages({
4
+ goBackButton: {
5
+ id: "goBackButton",
6
+ defaultMessage: "Go Back",
7
+ description:
8
+ "The label for the fallback secondary button when it doesn't exist",
9
+ },
10
+ });
@@ -0,0 +1 @@
1
+ export { FormMessage } from "./FormMessage";
@@ -0,0 +1,27 @@
1
+ import React from "react";
2
+ import { render } from "@testing-library/react-native";
3
+ import { FormMessageBanner } from "./FormMessageBanner";
4
+ import { FormBannerMessageType } from "../../types";
5
+
6
+ const noticeMessage = {
7
+ messageType: FormBannerMessageType.NoticeMessage,
8
+ message: "Take note of this information.",
9
+ };
10
+
11
+ const warningMessage = {
12
+ messageType: FormBannerMessageType.WarningMessage,
13
+ message: "Caution is warranted in this case.",
14
+ };
15
+
16
+ describe("FormMessageBanner", () => {
17
+ it.each([
18
+ [FormBannerMessageType.NoticeMessage, noticeMessage],
19
+ [FormBannerMessageType.WarningMessage, warningMessage],
20
+ ])("should render a %s", async (_messageType, message) => {
21
+ const { getByText } = render(
22
+ <FormMessageBanner bannerMessages={[message]} />,
23
+ );
24
+
25
+ expect(getByText(message.message)).toBeDefined();
26
+ });
27
+ });
@@ -0,0 +1,33 @@
1
+ import React from "react";
2
+ import { FormBannerMessage, FormBannerMessageType } from "../../types";
3
+ import { Banner, BannerTypes } from "../../../Banner";
4
+
5
+ interface FormMessageBannerProps {
6
+ bannerMessages?: FormBannerMessage[];
7
+ }
8
+
9
+ export function FormMessageBanner({
10
+ bannerMessages,
11
+ }: FormMessageBannerProps): JSX.Element {
12
+ return (
13
+ <>
14
+ {bannerMessages?.map((message, index) => (
15
+ <Banner
16
+ key={index}
17
+ text={message.message}
18
+ type={getBannerType(message.messageType)}
19
+ />
20
+ ))}
21
+ </>
22
+ );
23
+ }
24
+
25
+ function getBannerType(messageType: FormBannerMessageType): BannerTypes {
26
+ switch (messageType) {
27
+ case FormBannerMessageType.WarningMessage:
28
+ return "warning";
29
+ case FormBannerMessageType.NoticeMessage:
30
+ default:
31
+ return "notice";
32
+ }
33
+ }
@@ -0,0 +1 @@
1
+ export { FormMessageBanner } from "./FormMessageBanner";
@@ -0,0 +1,159 @@
1
+ import React from "react";
2
+ import { fireEvent, render, waitFor } from "@testing-library/react-native";
3
+ import { Host } from "react-native-portalize";
4
+ import { IconNames } from "@jobber/design";
5
+ import { FormSaveButton } from "./FormSaveButton";
6
+ import { messages } from "./messages";
7
+ import { messages as buttonGroupMessage } from "../../../ButtonGroup/messages";
8
+
9
+ interface TestSecondaryActionProp {
10
+ label: string;
11
+ icon?: IconNames | undefined;
12
+ handleAction: {
13
+ onBeforeSubmit?: jest.Mock;
14
+ onSubmit: () => Promise<void>;
15
+ onSubmitError?: () => void;
16
+ resetFormOnSubmit?: boolean;
17
+ };
18
+ destructive?: boolean;
19
+ }
20
+ interface TestFormSaveButtonProps {
21
+ primaryAction: () => Promise<void>;
22
+ loading: boolean;
23
+ label?: string;
24
+ secondaryAction?: TestSecondaryActionProp[];
25
+ setSecondaryActionLoading: (bool: boolean) => void;
26
+ }
27
+ jest.mock("react-hook-form", () => ({
28
+ ...jest.requireActual("react-hook-form"),
29
+ useFormContext: () => ({
30
+ reset: () => jest.fn(),
31
+ }),
32
+ }));
33
+
34
+ function ButtonGroupForTest(props: TestFormSaveButtonProps) {
35
+ return (
36
+ <Host>
37
+ <FormSaveButton
38
+ primaryAction={props.primaryAction}
39
+ loading={props.loading}
40
+ label={props.label}
41
+ setSecondaryActionLoading={props.setSecondaryActionLoading}
42
+ secondaryActions={props.secondaryAction}
43
+ />
44
+ </Host>
45
+ );
46
+ }
47
+
48
+ describe("the form save button is enabled", () => {
49
+ const loading = false;
50
+ it("renders the form save button with default label", () => {
51
+ const pressHandler = jest.fn();
52
+ const { getByLabelText } = render(
53
+ <ButtonGroupForTest
54
+ primaryAction={pressHandler}
55
+ loading={loading}
56
+ setSecondaryActionLoading={jest.fn()}
57
+ />,
58
+ );
59
+ const saveButton = getByLabelText("Save");
60
+ expect(saveButton).toBeTruthy();
61
+ });
62
+
63
+ it("renders a save button and calls the onPress handler when pressed", () => {
64
+ const pressHandler = jest.fn();
65
+ const saveButtonText = messages.saveButton.defaultMessage;
66
+ const { getByLabelText } = render(
67
+ <ButtonGroupForTest
68
+ primaryAction={pressHandler}
69
+ loading={loading}
70
+ setSecondaryActionLoading={jest.fn()}
71
+ />,
72
+ );
73
+
74
+ fireEvent.press(getByLabelText(saveButtonText));
75
+ expect(pressHandler).toHaveBeenCalled();
76
+ });
77
+
78
+ it("renders a save button with a custom label if provided", () => {
79
+ const pressHandler = jest.fn();
80
+ const saveButtonText = "MySave";
81
+ const { getByLabelText } = render(
82
+ <ButtonGroupForTest
83
+ primaryAction={pressHandler}
84
+ loading={loading}
85
+ setSecondaryActionLoading={jest.fn()}
86
+ label={saveButtonText}
87
+ />,
88
+ );
89
+ const saveButton = getByLabelText(saveButtonText);
90
+ expect(saveButton).toBeTruthy();
91
+ });
92
+ });
93
+
94
+ describe("the form save button is loading", () => {
95
+ const loading = true;
96
+ it("renders the form save button as loading", () => {
97
+ const pressHandler = jest.fn();
98
+ const { getByTestId, getByRole } = render(
99
+ <ButtonGroupForTest
100
+ primaryAction={pressHandler}
101
+ loading={loading}
102
+ setSecondaryActionLoading={jest.fn()}
103
+ />,
104
+ );
105
+ expect(getByTestId("loadingImage")).toBeDefined();
106
+ expect(getByRole("button", { busy: true })).toBeDefined();
107
+ });
108
+ });
109
+
110
+ describe("when a secondaryActions is passed in", () => {
111
+ it("renders a secondaryAction element", () => {
112
+ const pressHandler = jest.fn();
113
+ const { getByLabelText } = render(
114
+ <ButtonGroupForTest
115
+ primaryAction={pressHandler}
116
+ loading={false}
117
+ setSecondaryActionLoading={jest.fn()}
118
+ secondaryAction={[
119
+ { label: "hi", handleAction: { onSubmit: jest.fn() } },
120
+ ]}
121
+ />,
122
+ );
123
+
124
+ expect(
125
+ getByLabelText(buttonGroupMessage.more.defaultMessage),
126
+ ).toBeDefined();
127
+ });
128
+
129
+ it("renders a secondaryAction element with and fires the onSubmit and beforeSubmit if available", async () => {
130
+ const pressHandler = jest.fn(() => Promise.resolve());
131
+ const beforeSubmitMock = jest.fn().mockImplementation(() => {
132
+ return Promise.resolve(true);
133
+ });
134
+ const { getByLabelText } = render(
135
+ <ButtonGroupForTest
136
+ primaryAction={pressHandler}
137
+ loading={false}
138
+ setSecondaryActionLoading={jest.fn()}
139
+ secondaryAction={[
140
+ {
141
+ icon: "trash",
142
+ label: "hi",
143
+ handleAction: {
144
+ onSubmit: pressHandler,
145
+ onBeforeSubmit: beforeSubmitMock,
146
+ },
147
+ },
148
+ ]}
149
+ />,
150
+ );
151
+ fireEvent.press(getByLabelText(buttonGroupMessage.more.defaultMessage));
152
+ expect(getByLabelText("hi")).toBeDefined();
153
+ fireEvent.press(getByLabelText("hi"));
154
+ expect(beforeSubmitMock).toHaveBeenCalled();
155
+ await waitFor(() => {
156
+ expect(pressHandler).toHaveBeenCalled();
157
+ });
158
+ });
159
+ });
@@ -0,0 +1,103 @@
1
+ import React from "react";
2
+ import { useIntl } from "react-intl";
3
+ import { useFormContext } from "react-hook-form";
4
+ import { messages } from "./messages";
5
+ import { FormSaveButtonProps, SecondaryActionProp } from "../../types";
6
+ import {
7
+ ButtonGroup,
8
+ ButtonGroupSecondaryActionProps,
9
+ } from "../../../ButtonGroup";
10
+
11
+ export function FormSaveButton({
12
+ primaryAction,
13
+ loading,
14
+ label,
15
+ secondaryActions,
16
+ setSecondaryActionLoading,
17
+ onOpenBottomSheet,
18
+ onCloseBottomSheet,
19
+ }: FormSaveButtonProps): JSX.Element {
20
+ const { formatMessage } = useIntl();
21
+ const formContext = useFormContext();
22
+ const buttonActions = useButtonGroupAction(secondaryActions);
23
+
24
+ return (
25
+ <>
26
+ <ButtonGroup
27
+ onOpenBottomSheet={onOpenBottomSheet}
28
+ onCloseBottomSheet={onCloseBottomSheet}
29
+ allowTapWhenOffline={true}
30
+ >
31
+ {buttonActions.map((action, index) => {
32
+ if (index === 0) {
33
+ return (
34
+ <ButtonGroup.PrimaryAction
35
+ key={index}
36
+ onPress={primaryAction}
37
+ label={label ?? formatMessage(messages.saveButton)}
38
+ loading={loading}
39
+ />
40
+ );
41
+ } else {
42
+ return (
43
+ <ButtonGroup.SecondaryAction
44
+ key={index}
45
+ label={action.label}
46
+ icon={action.icon}
47
+ onPress={action.onPress}
48
+ destructive={action.destructive}
49
+ />
50
+ );
51
+ }
52
+ })}
53
+ </ButtonGroup>
54
+ </>
55
+ );
56
+
57
+ function useButtonGroupAction(
58
+ array: SecondaryActionProp[] | undefined,
59
+ ): ButtonGroupSecondaryActionProps[] {
60
+ const buttonGroupActionProps: ButtonGroupSecondaryActionProps[] = array
61
+ ? array.map(arr => {
62
+ return {
63
+ label: arr.label,
64
+ onPress: () => internalOnPress(arr.handleAction),
65
+ destructive: arr.destructive,
66
+ icon: arr.icon,
67
+ };
68
+ })
69
+ : [];
70
+
71
+ buttonGroupActionProps.unshift({
72
+ label: label ?? formatMessage(messages.saveButton),
73
+ onPress: primaryAction,
74
+ loading: loading,
75
+ icon: undefined,
76
+ });
77
+
78
+ return buttonGroupActionProps;
79
+ }
80
+
81
+ async function internalOnPress(
82
+ handleAction: SecondaryActionProp["handleAction"],
83
+ ) {
84
+ let performSubmit = true;
85
+ if (handleAction.onBeforeSubmit) {
86
+ performSubmit = await handleAction.onBeforeSubmit();
87
+ }
88
+
89
+ if (performSubmit) {
90
+ setSecondaryActionLoading?.(true);
91
+ handleAction
92
+ .onSubmit(primaryAction)
93
+ .then(() => {
94
+ handleAction.resetFormOnSubmit && formContext.reset();
95
+ handleAction.onSubmitSuccess?.();
96
+ })
97
+ .catch(handleAction.onSubmitError)
98
+ .finally(() => {
99
+ setSecondaryActionLoading?.(false);
100
+ });
101
+ }
102
+ }
103
+ }
@@ -0,0 +1 @@
1
+ export { FormSaveButton } from "./FormSaveButton";
@@ -0,0 +1,9 @@
1
+ import { defineMessages } from "react-intl";
2
+
3
+ export const messages = defineMessages({
4
+ saveButton: {
5
+ id: "saveButton",
6
+ defaultMessage: "Save",
7
+ description: "The label for the save button",
8
+ },
9
+ });
@@ -0,0 +1,2 @@
1
+ export const KEYBOARD_TOP_PADDING_AUTO_SCROLL = 20;
2
+ export const KEYBOARD_SAVE_BUTTON_DISTANCE = 1;
@@ -0,0 +1,45 @@
1
+ import React, { PropsWithChildren } from "react";
2
+ import { renderHook } from "@testing-library/react-hooks";
3
+ import { AtlantisFormContextProps } from "./types";
4
+ import {
5
+ AtlantisFormContext,
6
+ defaultValues,
7
+ useAtlantisFormContext,
8
+ } from "./AtlantisFormContext";
9
+
10
+ const useConfirmBeforeBackMock = jest.fn();
11
+ const useInternalFormLocalCacheMock = jest.fn();
12
+
13
+ const providerValues: AtlantisFormContextProps = {
14
+ useConfirmBeforeBack: useConfirmBeforeBackMock,
15
+ useInternalFormLocalCache: useInternalFormLocalCacheMock,
16
+ headerHeight: 50,
17
+ };
18
+
19
+ describe("AtlantisFormContext", () => {
20
+ beforeEach(() => {
21
+ jest.resetModules();
22
+ });
23
+
24
+ describe("No Provider", () => {
25
+ it("should get the default values", () => {
26
+ const { result } = renderHook(() => useAtlantisFormContext());
27
+
28
+ expect(result.current).toMatchObject(defaultValues);
29
+ });
30
+ });
31
+
32
+ describe("With Provider", () => {
33
+ it("should get the provider values", () => {
34
+ const { result } = renderHook(() => useAtlantisFormContext(), {
35
+ wrapper: ({ children }: PropsWithChildren) => (
36
+ <AtlantisFormContext.Provider value={providerValues}>
37
+ {children}
38
+ </AtlantisFormContext.Provider>
39
+ ),
40
+ });
41
+
42
+ expect(result.current).toMatchObject(providerValues);
43
+ });
44
+ });
45
+ });
@@ -0,0 +1,21 @@
1
+ import { createContext, useContext, useRef } from "react";
2
+ import { AtlantisFormContextProps } from "./types";
3
+
4
+ export const defaultValues = {
5
+ useConfirmBeforeBack: () => {
6
+ const ref = useRef(() => undefined);
7
+ return ref;
8
+ },
9
+ useInternalFormLocalCache: () => ({
10
+ setLocalCache: () => undefined,
11
+ removeLocalCache: () => undefined,
12
+ }),
13
+ headerHeight: 0,
14
+ };
15
+
16
+ export const AtlantisFormContext =
17
+ createContext<AtlantisFormContextProps>(defaultValues);
18
+
19
+ export function useAtlantisFormContext(): AtlantisFormContextProps {
20
+ return useContext(AtlantisFormContext);
21
+ }
@@ -0,0 +1,5 @@
1
+ export {
2
+ AtlantisFormContext,
3
+ useAtlantisFormContext,
4
+ } from "./AtlantisFormContext";
5
+ export type { AtlantisFormContextProps } from "./types";