@jobber/components-native 0.36.0 → 0.38.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 (43) hide show
  1. package/dist/src/ContentOverlay/ContentOverlay.js +144 -0
  2. package/dist/src/ContentOverlay/ContentOverlay.style.js +56 -0
  3. package/dist/src/ContentOverlay/hooks/useKeyboardVisibility.js +21 -0
  4. package/dist/src/ContentOverlay/hooks/useViewLayoutHeight.js +10 -0
  5. package/dist/src/ContentOverlay/index.js +1 -0
  6. package/dist/src/ContentOverlay/messages.js +8 -0
  7. package/dist/src/ContentOverlay/types.js +1 -0
  8. package/dist/src/Disclosure/Disclosure.js +50 -0
  9. package/dist/src/Disclosure/Disclosure.style.js +21 -0
  10. package/dist/src/Disclosure/constants.js +1 -0
  11. package/dist/src/Disclosure/index.js +1 -0
  12. package/dist/src/index.js +2 -0
  13. package/dist/tsconfig.tsbuildinfo +1 -1
  14. package/dist/types/src/ContentOverlay/ContentOverlay.d.ts +6 -0
  15. package/dist/types/src/ContentOverlay/ContentOverlay.style.d.ts +60 -0
  16. package/dist/types/src/ContentOverlay/hooks/useKeyboardVisibility.d.ts +6 -0
  17. package/dist/types/src/ContentOverlay/hooks/useViewLayoutHeight.d.ts +6 -0
  18. package/dist/types/src/ContentOverlay/index.d.ts +2 -0
  19. package/dist/types/src/ContentOverlay/messages.d.ts +7 -0
  20. package/dist/types/src/ContentOverlay/types.d.ts +87 -0
  21. package/dist/types/src/Disclosure/Disclosure.d.ts +35 -0
  22. package/dist/types/src/Disclosure/Disclosure.style.d.ts +19 -0
  23. package/dist/types/src/Disclosure/constants.d.ts +1 -0
  24. package/dist/types/src/Disclosure/index.d.ts +1 -0
  25. package/dist/types/src/index.d.ts +2 -0
  26. package/package.json +2 -2
  27. package/src/ContentOverlay/ContentOverlay.style.ts +70 -0
  28. package/src/ContentOverlay/ContentOverlay.test.tsx +371 -0
  29. package/src/ContentOverlay/ContentOverlay.tsx +295 -0
  30. package/src/ContentOverlay/hooks/useKeyboardVisibility.test.ts +42 -0
  31. package/src/ContentOverlay/hooks/useKeyboardVisibility.ts +36 -0
  32. package/src/ContentOverlay/hooks/useViewLayoutHeight.test.ts +56 -0
  33. package/src/ContentOverlay/hooks/useViewLayoutHeight.ts +18 -0
  34. package/src/ContentOverlay/index.ts +2 -0
  35. package/src/ContentOverlay/messages.ts +9 -0
  36. package/src/ContentOverlay/types.ts +96 -0
  37. package/src/Disclosure/Disclosure.style.ts +22 -0
  38. package/src/Disclosure/Disclosure.test.tsx +71 -0
  39. package/src/Disclosure/Disclosure.tsx +162 -0
  40. package/src/Disclosure/__snapshots__/Disclosure.test.tsx.snap +488 -0
  41. package/src/Disclosure/constants.ts +1 -0
  42. package/src/Disclosure/index.ts +1 -0
  43. package/src/index.ts +2 -0
@@ -0,0 +1,144 @@
1
+ import React, { forwardRef, useCallback, useImperativeHandle, useMemo, useRef, useState, } from "react";
2
+ import { Modalize } from "react-native-modalize";
3
+ import { useSafeAreaInsets } from "react-native-safe-area-context";
4
+ import { AccessibilityInfo, Platform, View, findNodeHandle, useWindowDimensions, } from "react-native";
5
+ import { Portal } from "react-native-portalize";
6
+ import { useIntl } from "react-intl";
7
+ import { useKeyboardVisibility } from "./hooks/useKeyboardVisibility";
8
+ import { styles } from "./ContentOverlay.style";
9
+ import { messages } from "./messages";
10
+ import { useViewLayoutHeight } from "./hooks/useViewLayoutHeight";
11
+ import { useIsScreenReaderEnabled } from "../hooks";
12
+ import { IconButton } from "../IconButton";
13
+ import { tokens } from "../utils/design";
14
+ import { Heading } from "../Heading";
15
+ export const ContentOverlay = forwardRef(ContentOverlayPortal);
16
+ const ContentOverlayModal = forwardRef(ContentOverlayInternal);
17
+ // eslint-disable-next-line max-statements
18
+ function ContentOverlayInternal({ children, title, accessibilityLabel, fullScreen = false, showDismiss = false, isDraggable = true, adjustToContentHeight = false, keyboardShouldPersistTaps = false, keyboardAvoidingBehavior, scrollEnabled = false, modalBackgroundColor = "surface", onClose, onOpen, onBeforeExit, loading = false, avoidKeyboardLikeIOS, }, ref) {
19
+ isDraggable = onBeforeExit ? false : isDraggable;
20
+ const isCloseableOnOverlayTap = onBeforeExit ? false : true;
21
+ const { formatMessage } = useIntl();
22
+ const { width: windowWidth, height: windowHeight } = useWindowDimensions();
23
+ const insets = useSafeAreaInsets();
24
+ const [position, setPosition] = useState("initial");
25
+ const isScreenReaderEnabled = useIsScreenReaderEnabled();
26
+ const isFullScreenOrTopPosition = fullScreen || (!adjustToContentHeight && position === "top");
27
+ const shouldShowDismiss = showDismiss || isScreenReaderEnabled || isFullScreenOrTopPosition;
28
+ const [showHeaderShadow, setShowHeaderShadow] = useState(false);
29
+ const overlayHeader = useRef();
30
+ const internalRef = useRef();
31
+ const [modalizeMethods, setModalizeMethods] = useState();
32
+ const callbackInternalRef = useCallback((instance) => {
33
+ if (instance && !internalRef.current) {
34
+ internalRef.current = instance;
35
+ setModalizeMethods(instance);
36
+ }
37
+ }, []);
38
+ const refMethods = useMemo(() => {
39
+ if (!(modalizeMethods === null || modalizeMethods === void 0 ? void 0 : modalizeMethods.open) || !(modalizeMethods === null || modalizeMethods === void 0 ? void 0 : modalizeMethods.close)) {
40
+ return {};
41
+ }
42
+ return {
43
+ open: modalizeMethods === null || modalizeMethods === void 0 ? void 0 : modalizeMethods.open,
44
+ close: modalizeMethods === null || modalizeMethods === void 0 ? void 0 : modalizeMethods.close,
45
+ };
46
+ }, [modalizeMethods]);
47
+ const { keyboardHeight } = useKeyboardVisibility();
48
+ useImperativeHandle(ref, () => refMethods, [refMethods]);
49
+ const { handleLayout: handleChildrenLayout, height: childrenHeight, heightKnown: childrenHeightKnown, } = useViewLayoutHeight();
50
+ const { handleLayout: handleHeaderLayout, height: headerHeight, heightKnown: headerHeightKnown, } = useViewLayoutHeight();
51
+ const snapPoint = useMemo(() => {
52
+ if (fullScreen || !isDraggable || adjustToContentHeight) {
53
+ return undefined;
54
+ }
55
+ const overlayHeight = headerHeight + childrenHeight;
56
+ if (overlayHeight >= windowHeight) {
57
+ return undefined;
58
+ }
59
+ return overlayHeight;
60
+ }, [
61
+ fullScreen,
62
+ isDraggable,
63
+ adjustToContentHeight,
64
+ headerHeight,
65
+ childrenHeight,
66
+ windowHeight,
67
+ ]);
68
+ const modalStyle = [
69
+ styles.modal,
70
+ windowWidth > 640 ? styles.modalForLargeScreens : undefined,
71
+ { backgroundColor: getModalBackgroundColor(modalBackgroundColor) },
72
+ keyboardHeight > 0 && { marginBottom: 0 },
73
+ ];
74
+ const renderedChildren = renderChildren();
75
+ const renderedHeader = renderHeader();
76
+ const onCloseController = () => {
77
+ var _a;
78
+ if (!onBeforeExit) {
79
+ (_a = internalRef.current) === null || _a === void 0 ? void 0 : _a.close();
80
+ return true;
81
+ }
82
+ else {
83
+ onBeforeExit();
84
+ return false;
85
+ }
86
+ };
87
+ return (React.createElement(React.Fragment, null,
88
+ headerHeightKnown && childrenHeightKnown && (React.createElement(Modalize, { ref: callbackInternalRef, overlayStyle: styles.overlay, handleStyle: styles.handle, handlePosition: "inside", modalStyle: modalStyle, modalTopOffset: tokens["space-larger"], snapPoint: snapPoint, closeSnapPointStraightEnabled: false, withHandle: isDraggable, panGestureEnabled: isDraggable, adjustToContentHeight: adjustToContentHeight, disableScrollIfPossible: !adjustToContentHeight, onClose: onClose, onOpen: onOpen, keyboardAvoidingBehavior: keyboardAvoidingBehavior, avoidKeyboardLikeIOS: avoidKeyboardLikeIOS, childrenStyle: styles.childrenStyle, onBackButtonPress: onCloseController, closeOnOverlayTap: isCloseableOnOverlayTap, onOpened: () => {
89
+ if (overlayHeader.current) {
90
+ const reactTag = findNodeHandle(overlayHeader.current);
91
+ if (reactTag) {
92
+ AccessibilityInfo.setAccessibilityFocus(reactTag);
93
+ }
94
+ }
95
+ }, scrollViewProps: {
96
+ scrollEnabled,
97
+ showsVerticalScrollIndicator: false,
98
+ stickyHeaderIndices: Platform.OS === "android" ? [0] : undefined,
99
+ onScroll: handleOnScroll,
100
+ keyboardShouldPersistTaps: keyboardShouldPersistTaps
101
+ ? "handled"
102
+ : "never",
103
+ }, HeaderComponent: Platform.OS === "ios" ? renderedHeader : undefined, onPositionChange: setPosition },
104
+ Platform.OS === "android" ? renderedHeader : undefined,
105
+ renderedChildren)),
106
+ !childrenHeightKnown && (React.createElement(View, { style: [styles.hiddenContent, modalStyle] }, renderedChildren)),
107
+ !headerHeightKnown && (React.createElement(View, { style: [styles.hiddenContent, modalStyle] }, renderedHeader))));
108
+ function renderHeader() {
109
+ const closeOverlayA11YLabel = formatMessage(messages.closeOverlayA11YLabel, {
110
+ title: title,
111
+ });
112
+ const headerStyles = [
113
+ styles.header,
114
+ showHeaderShadow && styles.headerShadow,
115
+ { backgroundColor: getModalBackgroundColor(modalBackgroundColor) },
116
+ ];
117
+ return (React.createElement(View, { onLayout: handleHeaderLayout, testID: "ATL-Overlay-Header" },
118
+ React.createElement(View, { style: headerStyles },
119
+ React.createElement(View, { style: showDismiss ? styles.titleWithDismiss : styles.titleWithoutDimiss },
120
+ React.createElement(Heading, { level: "subtitle", variation: loading ? "subdued" : "heading" }, title)),
121
+ shouldShowDismiss && (React.createElement(View, { style: styles.dismissButton,
122
+ // @ts-expect-error tsc-ci
123
+ ref: overlayHeader, accessibilityLabel: accessibilityLabel || closeOverlayA11YLabel, accessible: true },
124
+ React.createElement(IconButton, { name: "cross", customColor: loading ? tokens["color-disabled"] : tokens["color-heading"], onPress: () => onCloseController(), accessibilityLabel: closeOverlayA11YLabel, testID: "ATL-Overlay-CloseButton" }))))));
125
+ }
126
+ function renderChildren() {
127
+ return (React.createElement(View, { style: { paddingBottom: insets.bottom }, onLayout: handleChildrenLayout, testID: "ATL-Overlay-Children" }, children));
128
+ }
129
+ function handleOnScroll({ nativeEvent, }) {
130
+ setShowHeaderShadow(nativeEvent.contentOffset.y > 0);
131
+ }
132
+ function getModalBackgroundColor(variation) {
133
+ switch (variation) {
134
+ case "surface":
135
+ return tokens["color-surface"];
136
+ case "background":
137
+ return tokens["color-surface--background"];
138
+ }
139
+ }
140
+ }
141
+ function ContentOverlayPortal(modalProps, ref) {
142
+ return (React.createElement(Portal, null,
143
+ React.createElement(ContentOverlayModal, Object.assign({ ref: ref }, modalProps))));
144
+ }
@@ -0,0 +1,56 @@
1
+ import { StyleSheet } from "react-native";
2
+ import { tokens } from "../utils/design";
3
+ const modalBorderRadius = tokens["radius-larger"];
4
+ const titleOffsetFromHandle = tokens["space-small"] + tokens["space-smallest"];
5
+ export const styles = StyleSheet.create({
6
+ handle: {
7
+ width: tokens["space-largest"],
8
+ height: tokens["space-smaller"] + tokens["space-smallest"],
9
+ backgroundColor: tokens["color-border"],
10
+ top: tokens["space-small"],
11
+ borderRadius: tokens["radius-circle"],
12
+ },
13
+ overlay: {
14
+ backgroundColor: tokens["color-overlay"],
15
+ },
16
+ modal: {
17
+ borderTopLeftRadius: modalBorderRadius,
18
+ borderTopRightRadius: modalBorderRadius,
19
+ },
20
+ modalForLargeScreens: {
21
+ width: 640,
22
+ alignSelf: "center",
23
+ },
24
+ header: {
25
+ flexDirection: "row",
26
+ backgroundColor: tokens["color-surface"],
27
+ paddingTop: titleOffsetFromHandle,
28
+ zIndex: tokens["elevation-base"],
29
+ borderTopLeftRadius: modalBorderRadius,
30
+ borderTopRightRadius: modalBorderRadius,
31
+ height: tokens["space-extravagant"],
32
+ },
33
+ headerShadow: Object.assign({}, tokens["shadow-base"]),
34
+ childrenStyle: {
35
+ // We need to explicity lower the zIndex because otherwise, the modal content slides over the header shadow.
36
+ zIndex: -1,
37
+ },
38
+ dismissButton: {
39
+ alignItems: "center",
40
+ },
41
+ hiddenContent: {
42
+ opacity: 0,
43
+ },
44
+ titleWithoutDimiss: {
45
+ flex: 1,
46
+ flexDirection: "row",
47
+ justifyContent: "center",
48
+ paddingTop: tokens["space-base"],
49
+ },
50
+ titleWithDismiss: {
51
+ flex: 1,
52
+ justifyContent: "center",
53
+ paddingLeft: tokens["space-base"],
54
+ paddingRight: tokens["space-smaller"],
55
+ },
56
+ });
@@ -0,0 +1,21 @@
1
+ import { useEffect, useState } from "react";
2
+ import { Keyboard } from "react-native";
3
+ export function useKeyboardVisibility() {
4
+ const [isKeyboardVisible, setKeyboardVisible] = useState(false);
5
+ const [keyboardHeight, setKeyboardHeight] = useState(0);
6
+ useEffect(() => {
7
+ const keyboardDidShowListener = Keyboard.addListener("keyboardDidShow", (event) => {
8
+ setKeyboardVisible(true);
9
+ setKeyboardHeight(event.endCoordinates.height);
10
+ });
11
+ const keyboardDidHideListener = Keyboard.addListener("keyboardDidHide", () => {
12
+ setKeyboardVisible(false);
13
+ setKeyboardHeight(0);
14
+ });
15
+ return () => {
16
+ keyboardDidHideListener.remove();
17
+ keyboardDidShowListener.remove();
18
+ };
19
+ }, []);
20
+ return { isKeyboardVisible, keyboardHeight };
21
+ }
@@ -0,0 +1,10 @@
1
+ import { useState } from "react";
2
+ export function useViewLayoutHeight() {
3
+ const [heightKnown, setHeightKnown] = useState(false);
4
+ const [height, setHeight] = useState(0);
5
+ const handleLayout = ({ nativeEvent }) => {
6
+ setHeightKnown(true);
7
+ setHeight(nativeEvent.layout.height);
8
+ };
9
+ return { handleLayout, height, heightKnown };
10
+ }
@@ -0,0 +1 @@
1
+ export { ContentOverlay } from "./ContentOverlay";
@@ -0,0 +1,8 @@
1
+ import { defineMessages } from "react-intl";
2
+ export const messages = defineMessages({
3
+ closeOverlayA11YLabel: {
4
+ id: "closeOverlayA11yLabel",
5
+ defaultMessage: "Close {title} modal",
6
+ description: "Accessibility label for button to close the overlay modal",
7
+ },
8
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,50 @@
1
+ import React, { useState } from "react";
2
+ import { ScrollView, TouchableOpacity, View, } from "react-native";
3
+ import Reanimated, { Easing, useAnimatedStyle, useSharedValue, withTiming, } from "react-native-reanimated";
4
+ import { EASE_CUBIC_IN_OUT } from "./constants";
5
+ import { styles } from "./Disclosure.style";
6
+ import { tokens } from "../utils/design";
7
+ import { Icon } from "../Icon";
8
+ const ReanimatedView = Reanimated.createAnimatedComponent(View);
9
+ const ReanimatedScrollView = Reanimated.createAnimatedComponent(ScrollView);
10
+ export function Disclosure({ content, header, open, onToggle, isEmpty, animationDuration = tokens["timing-slowest"], }) {
11
+ return (React.createElement(View, { style: styles.container },
12
+ React.createElement(DisclosureHeader, Object.assign({}, { header, onToggle, isEmpty, open, animationDuration })),
13
+ React.createElement(DisclosureContent, Object.assign({}, { content, open, animationDuration }))));
14
+ }
15
+ function DisclosureHeader({ header, onToggle, isEmpty, open, animationDuration, }) {
16
+ const rotateZ = useSharedValue(0);
17
+ rotateZ.value = withTiming(open ? 0 : -180, {
18
+ easing: Easing.bezier(...EASE_CUBIC_IN_OUT),
19
+ duration: animationDuration,
20
+ });
21
+ const animatedStyle = useAnimatedStyle(() => {
22
+ return {
23
+ transform: [{ rotateZ: `${rotateZ.value}deg` }],
24
+ };
25
+ });
26
+ return (React.createElement(TouchableOpacity, { activeOpacity: tokens["opacity-pressed"], onPress: onToggle, disabled: isEmpty },
27
+ React.createElement(View, { style: styles.headerContainer },
28
+ header,
29
+ !isEmpty && (React.createElement(ReanimatedView, { style: [animatedStyle] },
30
+ React.createElement(Icon, { name: "arrowUp", color: "grey" }))))));
31
+ }
32
+ function DisclosureContent({ content, open, animationDuration, }) {
33
+ const [maxHeight, setMaxHeight] = useState(0);
34
+ const height = useSharedValue(0);
35
+ const onContentLayoutChange = (event) => {
36
+ const newHeight = event.nativeEvent.layout.height;
37
+ setMaxHeight(newHeight);
38
+ };
39
+ height.value = withTiming(open ? maxHeight : 0, {
40
+ duration: animationDuration,
41
+ easing: Easing.bezier(...EASE_CUBIC_IN_OUT),
42
+ });
43
+ const animatedStyle = useAnimatedStyle(() => {
44
+ return {
45
+ height: height.value,
46
+ };
47
+ }, []);
48
+ return (React.createElement(ReanimatedScrollView, { scrollEnabled: false, showsHorizontalScrollIndicator: false, showsVerticalScrollIndicator: false, style: [styles.contentContainer, animatedStyle] },
49
+ React.createElement(View, { testID: "content", onLayout: onContentLayoutChange }, content)));
50
+ }
@@ -0,0 +1,21 @@
1
+ import { StyleSheet } from "react-native";
2
+ import { tokens } from "../utils/design";
3
+ export const styles = StyleSheet.create({
4
+ container: {
5
+ width: "100%",
6
+ },
7
+ headerContainer: {
8
+ flexDirection: "row",
9
+ alignItems: "flex-start",
10
+ justifyContent: "space-between",
11
+ },
12
+ countColumn: {
13
+ paddingRight: tokens["space-base"],
14
+ },
15
+ titleContainer: {
16
+ flexDirection: "row",
17
+ },
18
+ contentContainer: {
19
+ paddingTop: tokens["space-small"],
20
+ },
21
+ });
@@ -0,0 +1 @@
1
+ export const EASE_CUBIC_IN_OUT = [0.645, 0.045, 0.355, 1.0];
@@ -0,0 +1 @@
1
+ export { Disclosure } from "./Disclosure";
package/dist/src/index.js CHANGED
@@ -10,6 +10,8 @@ export * from "./Card";
10
10
  export * from "./Checkbox";
11
11
  export * from "./Chip";
12
12
  export * from "./Content";
13
+ export * from "./ContentOverlay";
14
+ export * from "./Disclosure";
13
15
  export * from "./Divider";
14
16
  export * from "./EmptyState";
15
17
  export * from "./ErrorMessageWrapper";