@jobber/components-native 0.38.0 → 0.39.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 (55) hide show
  1. package/dist/src/AtlantisContext/AtlantisContext.js +2 -0
  2. package/dist/src/InputDate/InputDate.js +76 -0
  3. package/dist/src/InputDate/index.js +1 -0
  4. package/dist/src/InputDate/messages.js +8 -0
  5. package/dist/src/Menu/Menu.js +67 -0
  6. package/dist/src/Menu/Menu.style.js +6 -0
  7. package/dist/src/Menu/components/MenuOption/MenuOption.js +25 -0
  8. package/dist/src/Menu/components/MenuOption/MenuOption.style.js +10 -0
  9. package/dist/src/Menu/components/MenuOption/index.js +1 -0
  10. package/dist/src/Menu/components/Overlay/Overlay.js +9 -0
  11. package/dist/src/Menu/components/Overlay/Overlay.style.js +6 -0
  12. package/dist/src/Menu/components/Overlay/index.js +1 -0
  13. package/dist/src/Menu/index.js +1 -0
  14. package/dist/src/Menu/messages.js +8 -0
  15. package/dist/src/Menu/types.js +1 -0
  16. package/dist/src/Menu/utils.js +84 -0
  17. package/dist/src/index.js +2 -0
  18. package/dist/tsconfig.tsbuildinfo +1 -1
  19. package/dist/types/src/AtlantisContext/AtlantisContext.d.ts +7 -1
  20. package/dist/types/src/InputDate/InputDate.d.ts +74 -0
  21. package/dist/types/src/InputDate/index.d.ts +1 -0
  22. package/dist/types/src/InputDate/messages.d.ts +7 -0
  23. package/dist/types/src/Menu/Menu.d.ts +3 -0
  24. package/dist/types/src/Menu/Menu.style.d.ts +18 -0
  25. package/dist/types/src/Menu/components/MenuOption/MenuOption.d.ts +3 -0
  26. package/dist/types/src/Menu/components/MenuOption/MenuOption.style.d.ts +8 -0
  27. package/dist/types/src/Menu/components/MenuOption/index.d.ts +1 -0
  28. package/dist/types/src/Menu/components/Overlay/Overlay.d.ts +3 -0
  29. package/dist/types/src/Menu/components/Overlay/Overlay.style.d.ts +12 -0
  30. package/dist/types/src/Menu/components/Overlay/index.d.ts +1 -0
  31. package/dist/types/src/Menu/index.d.ts +2 -0
  32. package/dist/types/src/Menu/messages.d.ts +7 -0
  33. package/dist/types/src/Menu/types.d.ts +22 -0
  34. package/dist/types/src/Menu/utils.d.ts +10 -0
  35. package/dist/types/src/index.d.ts +2 -0
  36. package/package.json +2 -2
  37. package/src/AtlantisContext/AtlantisContext.tsx +10 -1
  38. package/src/InputDate/InputDate.test.tsx +295 -0
  39. package/src/InputDate/InputDate.tsx +231 -0
  40. package/src/InputDate/index.ts +1 -0
  41. package/src/InputDate/messages.ts +9 -0
  42. package/src/Menu/Menu.style.ts +16 -0
  43. package/src/Menu/Menu.test.tsx +201 -0
  44. package/src/Menu/Menu.tsx +116 -0
  45. package/src/Menu/components/MenuOption/MenuOption.style.tsx +11 -0
  46. package/src/Menu/components/MenuOption/MenuOption.tsx +63 -0
  47. package/src/Menu/components/MenuOption/index.ts +1 -0
  48. package/src/Menu/components/Overlay/Overlay.style.ts +13 -0
  49. package/src/Menu/components/Overlay/Overlay.tsx +16 -0
  50. package/src/Menu/components/Overlay/index.ts +1 -0
  51. package/src/Menu/index.ts +6 -0
  52. package/src/Menu/messages.ts +9 -0
  53. package/src/Menu/types.ts +25 -0
  54. package/src/Menu/utils.ts +151 -0
  55. package/src/index.ts +2 -0
@@ -3,6 +3,7 @@ import { createContext, useContext } from "react";
3
3
  import RNLocalize from "react-native-localize";
4
4
  import { DEFAULT_CURRENCY_SYMBOL } from "../InputCurrency/constants";
5
5
  export const defaultValues = {
6
+ dateFormat: "PP",
6
7
  // The system time is "p"
7
8
  timeFormat: "p",
8
9
  timeZone: RNLocalize.getTimeZone(),
@@ -12,6 +13,7 @@ export const defaultValues = {
12
13
  },
13
14
  floatSeparators: { group: ",", decimal: "." },
14
15
  currencySymbol: DEFAULT_CURRENCY_SYMBOL,
16
+ headerHeight: 0,
15
17
  };
16
18
  export const AtlantisContext = createContext(defaultValues);
17
19
  export function useAtlantisContext() {
@@ -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,8 @@
1
+ import { defineMessages } from "react-intl";
2
+ export const messages = defineMessages({
3
+ datePlaceholder: {
4
+ id: "datePlaceholder",
5
+ defaultMessage: "Date",
6
+ description: "Default input label for the InputDate component",
7
+ },
8
+ });
@@ -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,8 @@
1
+ import { defineMessages } from "react-intl";
2
+ export const messages = defineMessages({
3
+ more: {
4
+ id: "more",
5
+ defaultMessage: "Menu",
6
+ description: "Accessibility label to open the menu",
7
+ },
8
+ });
@@ -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
@@ -22,12 +22,14 @@ export * from "./Icon";
22
22
  export * from "./IconButton";
23
23
  export * from "./InputFieldWrapper";
24
24
  export * from "./InputCurrency";
25
+ export * from "./InputDate";
25
26
  export * from "./InputNumber";
26
27
  export * from "./InputPassword";
27
28
  export * from "./InputPressable";
28
29
  export * from "./InputSearch";
29
30
  export * from "./InputTime";
30
31
  export * from "./InputText";
32
+ export * from "./Menu";
31
33
  export * from "./TextList";
32
34
  export * from "./ProgressBar";
33
35
  export * from "./Select";