@jobber/components-native 0.98.5 → 0.100.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 (72) hide show
  1. package/dist/package.json +3 -6
  2. package/dist/src/AtlantisOverlayProvider/AtlantisOverlayProvider.js +5 -0
  3. package/dist/src/AtlantisOverlayProvider/index.js +1 -0
  4. package/dist/src/BottomSheet/BottomSheet.js +9 -11
  5. package/dist/src/BottomSheet/hooks/useBottomSheetBackHandler.js +2 -2
  6. package/dist/src/ButtonGroup/components/SecondaryActionSheet/SecondaryActionSheet.js +9 -11
  7. package/dist/src/ContentOverlay/BottomSheetKeyboardAwareScrollView.js +19 -0
  8. package/dist/src/ContentOverlay/ContentOverlay.js +143 -107
  9. package/dist/src/ContentOverlay/ContentOverlay.style.js +8 -12
  10. package/dist/src/ContentOverlay/computeContentOverlayBehavior.js +76 -0
  11. package/dist/src/ContentOverlay/constants.js +1 -0
  12. package/dist/src/ContentOverlay/hooks/useBottomSheetModalBackHandler.js +25 -0
  13. package/dist/src/ContentOverlay/index.js +1 -1
  14. package/dist/src/FormatFile/components/FormatFileBottomSheet/FormatFileBottomSheet.js +7 -9
  15. package/dist/src/InputText/InputText.js +44 -1
  16. package/dist/src/index.js +1 -0
  17. package/dist/src/utils/meta/meta.json +1 -0
  18. package/dist/tsconfig.build.tsbuildinfo +1 -1
  19. package/dist/types/src/AtlantisOverlayProvider/AtlantisOverlayProvider.d.ts +6 -0
  20. package/dist/types/src/AtlantisOverlayProvider/index.d.ts +1 -0
  21. package/dist/types/src/BottomSheet/hooks/useBottomSheetBackHandler.d.ts +3 -3
  22. package/dist/types/src/ContentOverlay/BottomSheetKeyboardAwareScrollView.d.ts +11 -0
  23. package/dist/types/src/ContentOverlay/ContentOverlay.d.ts +2 -5
  24. package/dist/types/src/ContentOverlay/ContentOverlay.style.d.ts +11 -10
  25. package/dist/types/src/ContentOverlay/computeContentOverlayBehavior.d.ts +32 -0
  26. package/dist/types/src/ContentOverlay/constants.d.ts +1 -0
  27. package/dist/types/src/ContentOverlay/hooks/useBottomSheetModalBackHandler.d.ts +7 -0
  28. package/dist/types/src/ContentOverlay/index.d.ts +1 -1
  29. package/dist/types/src/ContentOverlay/types.d.ts +5 -12
  30. package/dist/types/src/index.d.ts +1 -0
  31. package/jestSetup.js +2 -0
  32. package/package.json +3 -6
  33. package/src/AtlantisOverlayProvider/AtlantisOverlayProvider.tsx +12 -0
  34. package/src/AtlantisOverlayProvider/index.ts +1 -0
  35. package/src/BottomSheet/BottomSheet.tsx +13 -13
  36. package/src/BottomSheet/hooks/useBottomSheetBackHandler.test.ts +10 -10
  37. package/src/BottomSheet/hooks/useBottomSheetBackHandler.ts +4 -4
  38. package/src/ButtonGroup/ButtonGroup.stories.tsx +10 -8
  39. package/src/ButtonGroup/ButtonGroup.test.tsx +7 -10
  40. package/src/ButtonGroup/components/SecondaryActionSheet/SecondaryActionSheet.tsx +26 -29
  41. package/src/ContentOverlay/BottomSheetKeyboardAwareScrollView.tsx +36 -0
  42. package/src/ContentOverlay/ContentOverlay.stories.tsx +32 -36
  43. package/src/ContentOverlay/ContentOverlay.style.ts +12 -12
  44. package/src/ContentOverlay/ContentOverlay.test.tsx +157 -79
  45. package/src/ContentOverlay/ContentOverlay.tsx +247 -205
  46. package/src/ContentOverlay/computeContentOverlayBehavior.test.ts +276 -0
  47. package/src/ContentOverlay/computeContentOverlayBehavior.ts +119 -0
  48. package/src/ContentOverlay/constants.ts +1 -0
  49. package/src/ContentOverlay/hooks/useBottomSheetModalBackHandler.test.ts +81 -0
  50. package/src/ContentOverlay/hooks/useBottomSheetModalBackHandler.ts +36 -0
  51. package/src/ContentOverlay/index.ts +4 -1
  52. package/src/ContentOverlay/types.ts +5 -13
  53. package/src/Form/Form.stories.tsx +8 -4
  54. package/src/Form/Form.test.tsx +51 -54
  55. package/src/Form/components/FormSaveButton/FormSaveButton.test.tsx +7 -10
  56. package/src/FormatFile/FormatFile.stories.tsx +3 -4
  57. package/src/FormatFile/FormatFile.test.tsx +11 -14
  58. package/src/FormatFile/components/FormatFileBottomSheet/FormatFileBottomSheet.test.tsx +6 -9
  59. package/src/FormatFile/components/FormatFileBottomSheet/FormatFileBottomSheet.tsx +21 -24
  60. package/src/InputDate/InputDate.test.tsx +5 -8
  61. package/src/InputText/InputText.test.tsx +122 -0
  62. package/src/InputText/InputText.tsx +62 -2
  63. package/src/InputTime/InputTime.stories.tsx +8 -4
  64. package/src/InputTime/InputTime.test.tsx +5 -8
  65. package/src/ThumbnailList/ThumbnailList.stories.tsx +6 -4
  66. package/src/ThumbnailList/ThumbnailList.test.tsx +5 -8
  67. package/src/ThumbnailList/__snapshots__/ThumbnailList.test.tsx.snap +101 -150
  68. package/src/index.ts +1 -0
  69. package/src/utils/meta/meta.json +2 -1
  70. package/dist/src/ContentOverlay/UNSAFE_WrappedModalize.js +0 -23
  71. package/dist/types/src/ContentOverlay/UNSAFE_WrappedModalize.d.ts +0 -3
  72. package/src/ContentOverlay/UNSAFE_WrappedModalize.tsx +0 -41
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jobber/components-native",
3
- "version": "0.98.5",
3
+ "version": "0.100.0",
4
4
  "license": "MIT",
5
5
  "description": "React Native implementation of Atlantis",
6
6
  "repository": {
@@ -46,8 +46,6 @@
46
46
  "react-hook-form": "^7.52.0",
47
47
  "react-intl": "^7.1.11",
48
48
  "react-native-keyboard-controller": "^1.20.7",
49
- "react-native-modalize": "^2.0.13",
50
- "react-native-portalize": "^1.0.7",
51
49
  "react-native-toast-message": "^2.1.6",
52
50
  "react-native-uuid": "^1.4.9",
53
51
  "ts-xor": "^1.1.0"
@@ -69,6 +67,7 @@
69
67
  "date-fns-tz": "^2.0.0",
70
68
  "react-native": "^0.82.1",
71
69
  "react-native-gesture-handler": "^2.29.1",
70
+ "react-native-keyboard-controller": "^1.12.0",
72
71
  "react-native-modal-datetime-picker": "^18.0.0",
73
72
  "react-native-reanimated": "^3.7.1",
74
73
  "react-native-safe-area-context": "^5.4.0",
@@ -90,11 +89,9 @@
90
89
  "react-native-gesture-handler": ">=2.22.0",
91
90
  "react-native-keyboard-controller": "^1.20.7",
92
91
  "react-native-modal-datetime-picker": " >=13.0.0",
93
- "react-native-modalize": "^2.0.13",
94
- "react-native-portalize": "^1.0.7",
95
92
  "react-native-reanimated": "^3.0.0",
96
93
  "react-native-safe-area-context": "^5.4.0",
97
94
  "react-native-svg": ">=12.0.0"
98
95
  },
99
- "gitHead": "60dbce9880c63f02f43d8b3f1f5ef9090dba32e3"
96
+ "gitHead": "2ef76c22620e1627eca8cf576b35e78228262574"
100
97
  }
@@ -0,0 +1,5 @@
1
+ import React from "react";
2
+ import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
3
+ export function AtlantisOverlayProvider({ children, }) {
4
+ return React.createElement(BottomSheetModalProvider, null, children);
5
+ }
@@ -0,0 +1 @@
1
+ export { AtlantisOverlayProvider } from "./AtlantisOverlayProvider";
@@ -1,7 +1,7 @@
1
1
  import React, { useCallback, useImperativeHandle, useRef } from "react";
2
2
  import { Keyboard, View } from "react-native";
3
3
  import { useSafeAreaInsets } from "react-native-safe-area-context";
4
- import RNBottomSheet, { BottomSheetBackdrop, BottomSheetView, } from "@gorhom/bottom-sheet";
4
+ import { BottomSheetBackdrop, BottomSheetModal, BottomSheetView, } from "@gorhom/bottom-sheet";
5
5
  import { tokens } from "@jobber/design";
6
6
  import { useStyles } from "./BottomSheet.style";
7
7
  import { BottomSheetOption } from "./components/BottomSheetOption";
@@ -22,7 +22,8 @@ export function BottomSheet({ children, showCancel, loading = false, heading, on
22
22
  useImperativeHandle(ref, () => ({
23
23
  open: () => {
24
24
  var _a;
25
- (_a = bottomSheetRef.current) === null || _a === void 0 ? void 0 : _a.expand();
25
+ dismissKeyboard();
26
+ (_a = bottomSheetRef.current) === null || _a === void 0 ? void 0 : _a.present();
26
27
  },
27
28
  close: () => {
28
29
  close();
@@ -30,25 +31,22 @@ export function BottomSheet({ children, showCancel, loading = false, heading, on
30
31
  }));
31
32
  const close = useCallback(() => {
32
33
  var _a;
33
- (_a = bottomSheetRef.current) === null || _a === void 0 ? void 0 : _a.close();
34
+ (_a = bottomSheetRef.current) === null || _a === void 0 ? void 0 : _a.dismiss();
34
35
  }, []);
35
36
  const handleChange = (index) => {
36
37
  // Handle Android back button
37
38
  handleSheetPositionChange(index);
38
39
  const previousIndex = previousIndexRef.current;
39
40
  if (previousIndex === -1 && index >= 0) {
40
- // Transitioned from closed to open
41
- dismissKeyboard();
42
41
  onOpen === null || onOpen === void 0 ? void 0 : onOpen();
43
42
  }
44
- else if (previousIndex >= 0 && index === -1) {
45
- // Transitioned from open to closed
46
- dismissKeyboard();
47
- onClose === null || onClose === void 0 ? void 0 : onClose();
48
- }
49
43
  previousIndexRef.current = index;
50
44
  };
51
- return (React.createElement(RNBottomSheet, { ref: bottomSheetRef, index: -1, backdropComponent: Backdrop, backgroundStyle: styles.background, enablePanDownToClose: true, onChange: handleChange, keyboardBlurBehavior: "restore", handleStyle: styles.handle },
45
+ return (React.createElement(BottomSheetModal, { ref: bottomSheetRef, backdropComponent: Backdrop, backgroundStyle: styles.background, enablePanDownToClose: true, onChange: handleChange, onDismiss: () => {
46
+ previousIndexRef.current = -1;
47
+ dismissKeyboard();
48
+ onClose === null || onClose === void 0 ? void 0 : onClose();
49
+ }, keyboardBlurBehavior: "restore", handleStyle: styles.handle },
52
50
  React.createElement(BottomSheetView, { style: {
53
51
  paddingBottom: insets.bottom + tokens["space-small"],
54
52
  paddingTop: tokens["space-small"],
@@ -1,7 +1,7 @@
1
1
  import { useCallback, useRef } from "react";
2
2
  import { BackHandler } from "react-native";
3
3
  /**
4
- * Hook that closes the bottom sheet on the hardware back button press if it is visible
4
+ * Hook that dismisses the bottom sheet modal on the hardware back button press if it is visible
5
5
  * @param bottomSheetRef ref to the bottom sheet component
6
6
  */
7
7
  export function useBottomSheetBackHandler(bottomSheetRef) {
@@ -13,7 +13,7 @@ export function useBottomSheetBackHandler(bottomSheetRef) {
13
13
  // Setup the back handler if the bottom sheet is right in front of the user
14
14
  backHandlerSubscriptionRef.current = BackHandler.addEventListener("hardwareBackPress", () => {
15
15
  var _a;
16
- (_a = bottomSheetRef.current) === null || _a === void 0 ? void 0 : _a.close();
16
+ (_a = bottomSheetRef.current) === null || _a === void 0 ? void 0 : _a.dismiss();
17
17
  return true;
18
18
  });
19
19
  }
@@ -1,17 +1,15 @@
1
1
  import React from "react";
2
2
  import { View } from "react-native";
3
- import { Portal } from "react-native-portalize";
4
3
  import { BottomSheetOption } from "../../../BottomSheet/components/BottomSheetOption";
5
4
  import { BottomSheet } from "../../../BottomSheet/BottomSheet";
6
5
  export function SecondaryActionSheet({ actions, secondaryActionsRef, heading, showCancel, onOpenBottomSheet, onCloseBottomSheet, }) {
7
- return (React.createElement(Portal, null,
8
- React.createElement(BottomSheet, { heading: heading, showCancel: showCancel, ref: secondaryActionsRef, onOpen: onOpenBottomSheet, onClose: onCloseBottomSheet },
9
- React.createElement(View, null, actions.map((action, index) => {
10
- const { label, onPress, icon, iconColor, destructive } = action;
11
- return (React.createElement(BottomSheetOption, { destructive: destructive, key: index, text: label, onPress: () => {
12
- var _a;
13
- (_a = secondaryActionsRef === null || secondaryActionsRef === void 0 ? void 0 : secondaryActionsRef.current) === null || _a === void 0 ? void 0 : _a.close();
14
- onPress();
15
- }, icon: icon, iconColor: iconColor }));
16
- })))));
6
+ return (React.createElement(BottomSheet, { heading: heading, showCancel: showCancel, ref: secondaryActionsRef, onOpen: onOpenBottomSheet, onClose: onCloseBottomSheet },
7
+ React.createElement(View, null, actions.map((action, index) => {
8
+ const { label, onPress, icon, iconColor, destructive } = action;
9
+ return (React.createElement(BottomSheetOption, { destructive: destructive, key: index, text: label, onPress: () => {
10
+ var _a;
11
+ (_a = secondaryActionsRef === null || secondaryActionsRef === void 0 ? void 0 : secondaryActionsRef.current) === null || _a === void 0 ? void 0 : _a.close();
12
+ onPress();
13
+ }, icon: icon, iconColor: iconColor }));
14
+ }))));
17
15
  }
@@ -0,0 +1,19 @@
1
+ import { memo } from "react";
2
+ import { KeyboardAwareScrollView } from "react-native-keyboard-controller";
3
+ import { SCROLLABLE_TYPE, createBottomSheetScrollableComponent, } from "@gorhom/bottom-sheet";
4
+ import Reanimated from "react-native-reanimated";
5
+ /**
6
+ * A keyboard-aware scroll view component that integrates with @gorhom/bottom-sheet.
7
+ *
8
+ * This component wraps `KeyboardAwareScrollView` from `react-native-keyboard-controller`
9
+ * with the bottom sheet HOCs to ensure proper keyboard handling on Android when using
10
+ * TextInputs inside a BottomSheet.
11
+ *
12
+ * @see https://kirillzyusko.github.io/react-native-keyboard-controller/docs/api/components/keyboard-aware-scroll-view#gorhombottom-sheet
13
+ */
14
+ const AnimatedScrollView = Reanimated.createAnimatedComponent(KeyboardAwareScrollView);
15
+ const BottomSheetScrollViewComponent = createBottomSheetScrollableComponent(SCROLLABLE_TYPE.SCROLLVIEW, AnimatedScrollView);
16
+ const BottomSheetKeyboardAwareScrollView = memo(BottomSheetScrollViewComponent);
17
+ BottomSheetKeyboardAwareScrollView.displayName =
18
+ "BottomSheetKeyboardAwareScrollView";
19
+ export { BottomSheetKeyboardAwareScrollView };
@@ -1,121 +1,162 @@
1
- import React, { forwardRef, useCallback, useImperativeHandle, useMemo, useRef, useState, } from "react";
1
+ var __rest = (this && this.__rest) || function (s, e) {
2
+ var t = {};
3
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
4
+ t[p] = s[p];
5
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
6
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
7
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
8
+ t[p[i]] = s[p[i]];
9
+ }
10
+ return t;
11
+ };
12
+ import React, { createContext, useContext, useImperativeHandle, useMemo, useRef, useState, } from "react";
13
+ import { AccessibilityInfo, View, findNodeHandle, useWindowDimensions, } from "react-native";
2
14
  import { useSafeAreaInsets } from "react-native-safe-area-context";
3
- import { AccessibilityInfo, Platform, View, findNodeHandle, useWindowDimensions, } from "react-native";
4
- import { Portal } from "react-native-portalize";
5
- import { useKeyboardVisibility } from "./hooks/useKeyboardVisibility";
15
+ import { BottomSheetBackdrop, BottomSheetModal, BottomSheetView, } from "@gorhom/bottom-sheet";
16
+ import { BottomSheetKeyboardAwareScrollView } from "./BottomSheetKeyboardAwareScrollView";
6
17
  import { useStyles } from "./ContentOverlay.style";
7
- import { useViewLayoutHeight } from "./hooks/useViewLayoutHeight";
8
- import { UNSAFE_WrappedModalize } from "./UNSAFE_WrappedModalize";
18
+ import { useBottomSheetModalBackHandler } from "./hooks/useBottomSheetModalBackHandler";
19
+ import { computeContentOverlayBehavior } from "./computeContentOverlayBehavior";
20
+ import { KEYBOARD_TOP_PADDING_AUTO_SCROLL } from "./constants";
9
21
  import { useIsScreenReaderEnabled } from "../hooks";
10
22
  import { IconButton } from "../IconButton";
11
23
  import { Heading } from "../Heading";
12
24
  import { useAtlantisI18n } from "../hooks/useAtlantisI18n";
13
25
  import { useAtlantisTheme } from "../AtlantisThemeContext";
14
- export const ContentOverlay = forwardRef(ContentOverlayPortal);
15
- const ContentOverlayModal = forwardRef(ContentOverlayInternal);
26
+ /**
27
+ * Signals whether keyboard handling inside a ContentOverlay is delegated to
28
+ * a keyboard-aware scroll view (e.g. BottomSheetKeyboardAwareScrollView).
29
+ *
30
+ * When `true`, InputText skips registering with the bottom-sheet's internal
31
+ * keyboard state so that only the scroll view manages keyboard offset —
32
+ * preventing double-counted spacing.
33
+ */
34
+ const ContentOverlayKeyboardContext = createContext(false);
35
+ export function useIsKeyboardHandledByScrollView() {
36
+ return useContext(ContentOverlayKeyboardContext);
37
+ }
38
+ const LARGE_SCREEN_BREAKPOINT = 640;
39
+ function getModalBackgroundColor(variation, tokens) {
40
+ switch (variation) {
41
+ case "surface":
42
+ return tokens["color-surface"];
43
+ case "background":
44
+ return tokens["color-surface--background"];
45
+ }
46
+ }
16
47
  // eslint-disable-next-line max-statements
17
- 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) {
18
- isDraggable = onBeforeExit ? false : isDraggable;
19
- const isCloseableOnOverlayTap = onBeforeExit ? false : true;
48
+ export function ContentOverlay({ children, title, accessibilityLabel, fullScreen = false, showDismiss = false, isDraggable = true, adjustToContentHeight = false, keyboardShouldPersistTaps = false, scrollEnabled = false, modalBackgroundColor = "surface", onClose, onOpen, onBeforeExit, loading = false, ref, }) {
49
+ const insets = useSafeAreaInsets();
50
+ const { width: windowWidth } = useWindowDimensions();
51
+ const bottomSheetModalRef = useRef(null);
52
+ const previousIndexRef = useRef(-1);
53
+ const [currentPosition, setCurrentPosition] = useState(-1);
54
+ const styles = useStyles();
20
55
  const { t } = useAtlantisI18n();
21
56
  const { tokens } = useAtlantisTheme();
22
- const { width: windowWidth, height: windowHeight } = useWindowDimensions();
23
- const insets = useSafeAreaInsets();
24
- const [position, setPosition] = useState("initial");
25
57
  const isScreenReaderEnabled = useIsScreenReaderEnabled();
26
- const isFullScreenOrTopPosition = fullScreen || (!adjustToContentHeight && position === "top");
27
- const shouldShowDismiss = showDismiss || isScreenReaderEnabled || isFullScreenOrTopPosition;
58
+ const behavior = computeContentOverlayBehavior({
59
+ fullScreen,
60
+ adjustToContentHeight,
61
+ isDraggable,
62
+ hasOnBeforeExit: onBeforeExit !== undefined,
63
+ showDismiss,
64
+ }, {
65
+ isScreenReaderEnabled,
66
+ position: currentPosition,
67
+ });
68
+ const effectiveIsDraggable = behavior.isDraggable;
69
+ const shouldShowDismiss = behavior.showDismiss;
70
+ const isCloseableOnOverlayTap = onBeforeExit === undefined;
71
+ // Prevent the Overlay from being flush with the top of the screen, even if we are "100%" or "fullscreen"
72
+ const topInset = insets.top || tokens["space-larger"];
28
73
  const [showHeaderShadow, setShowHeaderShadow] = useState(false);
29
74
  const overlayHeader = useRef(null);
30
- const internalRef = useRef(null);
31
- const [modalizeMethods, setModalizeMethods] = useState();
32
- const callbackInternalRef = useCallback((instance) => {
33
- if (instance && !internalRef.current) {
34
- internalRef.current = instance;
35
- setModalizeMethods(instance);
36
- }
75
+ const scrollViewRef = useRef(null);
76
+ // enableDynamicSizing will add another snap point of the content height
77
+ const snapPoints = useMemo(() => {
78
+ // There is a bug with "restore" behavior after keyboard is dismissed.
79
+ // https://github.com/gorhom/react-native-bottom-sheet/issues/2465
80
+ // providing a 100% snap point "fixes" it for now, but there is an approved PR to fix it
81
+ // that just needs to be merged and released: https://github.com/gorhom/react-native-bottom-sheet/pull/2511
82
+ return ["100%"];
37
83
  }, []);
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 styles = useStyles();
69
- const modalStyle = [
70
- styles.modal,
71
- windowWidth > 640 ? styles.modalForLargeScreens : undefined,
72
- { backgroundColor: getModalBackgroundColor(modalBackgroundColor) },
73
- keyboardHeight > 0 && { marginBottom: 0 },
74
- ];
75
- const renderedChildren = renderChildren();
76
- const renderedHeader = renderHeader();
77
84
  const onCloseController = () => {
78
85
  var _a;
79
86
  if (!onBeforeExit) {
80
- (_a = internalRef.current) === null || _a === void 0 ? void 0 : _a.close();
81
- return true;
87
+ (_a = bottomSheetModalRef.current) === null || _a === void 0 ? void 0 : _a.dismiss();
82
88
  }
83
89
  else {
84
90
  onBeforeExit();
85
- return false;
86
91
  }
87
92
  };
88
- return (React.createElement(React.Fragment, null,
89
- headerHeightKnown && childrenHeightKnown && (React.createElement(UNSAFE_WrappedModalize, { 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: () => {
90
- if (overlayHeader.current) {
91
- const reactTag = findNodeHandle(overlayHeader.current);
92
- if (reactTag) {
93
- AccessibilityInfo.setAccessibilityFocus(reactTag);
94
- }
93
+ const { handleSheetPositionChange } = useBottomSheetModalBackHandler(onCloseController);
94
+ useImperativeHandle(ref, () => ({
95
+ open: () => {
96
+ var _a;
97
+ (_a = bottomSheetModalRef.current) === null || _a === void 0 ? void 0 : _a.present();
98
+ },
99
+ close: () => {
100
+ var _a;
101
+ (_a = bottomSheetModalRef.current) === null || _a === void 0 ? void 0 : _a.dismiss();
102
+ },
103
+ }), []);
104
+ const handleChange = (index, position) => {
105
+ const previousIndex = previousIndexRef.current;
106
+ setCurrentPosition(position);
107
+ handleSheetPositionChange(index);
108
+ if (previousIndex === -1 && index >= 0) {
109
+ // Transitioned from closed to open
110
+ onOpen === null || onOpen === void 0 ? void 0 : onOpen();
111
+ // Set accessibility focus on header when opened
112
+ if (overlayHeader.current) {
113
+ const reactTag = findNodeHandle(overlayHeader.current);
114
+ if (reactTag) {
115
+ AccessibilityInfo.setAccessibilityFocus(reactTag);
95
116
  }
96
- }, scrollViewProps: {
97
- scrollEnabled,
98
- showsVerticalScrollIndicator: false,
99
- stickyHeaderIndices: Platform.OS === "android" ? [0] : undefined,
100
- onScroll: handleOnScroll,
101
- keyboardShouldPersistTaps: keyboardShouldPersistTaps
102
- ? "handled"
103
- : "never",
104
- }, HeaderComponent: Platform.OS === "ios" ? renderedHeader : undefined, onPositionChange: setPosition },
105
- Platform.OS === "android" ? renderedHeader : undefined,
106
- renderedChildren)),
107
- !childrenHeightKnown && (React.createElement(View, { style: [styles.hiddenContent, modalStyle] }, renderedChildren)),
108
- !headerHeightKnown && (React.createElement(View, { style: [styles.hiddenContent, modalStyle] }, renderedHeader))));
109
- function renderHeader() {
117
+ }
118
+ }
119
+ previousIndexRef.current = index;
120
+ };
121
+ const handleOnScroll = () => {
122
+ var _a;
123
+ const scrollTop = ((_a = scrollViewRef.current) === null || _a === void 0 ? void 0 : _a.scrollTop) || 0;
124
+ setShowHeaderShadow(scrollTop > 0);
125
+ };
126
+ const sheetStyle = useMemo(() => windowWidth > LARGE_SCREEN_BREAKPOINT
127
+ ? {
128
+ width: LARGE_SCREEN_BREAKPOINT,
129
+ marginLeft: (windowWidth - LARGE_SCREEN_BREAKPOINT) / 2,
130
+ }
131
+ : undefined, [windowWidth]);
132
+ const backgroundStyle = [
133
+ styles.background,
134
+ { backgroundColor: getModalBackgroundColor(modalBackgroundColor, tokens) },
135
+ ];
136
+ const handleIndicatorStyles = [
137
+ styles.handle,
138
+ !effectiveIsDraggable && {
139
+ opacity: 0,
140
+ },
141
+ ];
142
+ const renderHeader = () => {
110
143
  const closeOverlayA11YLabel = t("ContentOverlay.close", {
111
144
  title: title,
112
145
  });
113
146
  const headerStyles = [
114
147
  styles.header,
148
+ {
149
+ // Background color is necessary for scrollable modals as the content flows behind the header.
150
+ backgroundColor: getModalBackgroundColor(modalBackgroundColor, tokens),
151
+ },
152
+ ];
153
+ const headerShadowStyles = [
115
154
  showHeaderShadow && styles.headerShadow,
116
- { backgroundColor: getModalBackgroundColor(modalBackgroundColor) },
155
+ {
156
+ backgroundColor: getModalBackgroundColor(modalBackgroundColor, tokens),
157
+ },
117
158
  ];
118
- return (React.createElement(View, { onLayout: handleHeaderLayout, testID: "ATL-Overlay-Header" },
159
+ return (React.createElement(View, { testID: "ATL-Overlay-Header" },
119
160
  React.createElement(View, { style: headerStyles },
120
161
  React.createElement(View, { style: [
121
162
  styles.title,
@@ -125,24 +166,19 @@ function ContentOverlayInternal({ children, title, accessibilityLabel, fullScree
125
166
  ] },
126
167
  React.createElement(Heading, { level: "subtitle", variation: loading ? "subdued" : "heading", align: "start" }, title)),
127
168
  shouldShowDismiss && (React.createElement(View, { style: styles.dismissButton, ref: overlayHeader, accessibilityLabel: accessibilityLabel || closeOverlayA11YLabel, accessible: true },
128
- React.createElement(IconButton, { name: "cross", customColor: loading ? tokens["color-disabled"] : tokens["color-heading"], onPress: () => onCloseController(), accessibilityLabel: closeOverlayA11YLabel, testID: "ATL-Overlay-CloseButton" }))))));
129
- }
130
- function renderChildren() {
131
- return (React.createElement(View, { style: { paddingBottom: insets.bottom }, onLayout: handleChildrenLayout, testID: "ATL-Overlay-Children" }, children));
132
- }
133
- function handleOnScroll({ nativeEvent, }) {
134
- setShowHeaderShadow(nativeEvent.contentOffset.y > 0);
135
- }
136
- function getModalBackgroundColor(variation) {
137
- switch (variation) {
138
- case "surface":
139
- return tokens["color-surface"];
140
- case "background":
141
- return tokens["color-surface--background"];
142
- }
143
- }
169
+ React.createElement(IconButton, { name: "cross", customColor: loading ? tokens["color-disabled"] : tokens["color-heading"], onPress: () => onCloseController(), accessibilityLabel: closeOverlayA11YLabel, testID: "ATL-Overlay-CloseButton" })))),
170
+ React.createElement(View, null,
171
+ React.createElement(View, { style: headerShadowStyles }))));
172
+ };
173
+ return (React.createElement(BottomSheetModal, { ref: bottomSheetModalRef, onChange: handleChange, style: sheetStyle, backgroundStyle: backgroundStyle, handleStyle: styles.handleWrapper, handleIndicatorStyle: handleIndicatorStyles, backdropComponent: props => (React.createElement(Backdrop, Object.assign({}, props, { pressBehavior: isCloseableOnOverlayTap ? "close" : "none" }))), snapPoints: snapPoints, enablePanDownToClose: effectiveIsDraggable, enableContentPanningGesture: effectiveIsDraggable, enableHandlePanningGesture: effectiveIsDraggable, enableDynamicSizing: behavior.initialHeight === "contentHeight", keyboardBehavior: "interactive", keyboardBlurBehavior: "restore", topInset: topInset, onDismiss: () => onClose === null || onClose === void 0 ? void 0 : onClose() },
174
+ React.createElement(ContentOverlayKeyboardContext.Provider, { value: scrollEnabled }, scrollEnabled ? (React.createElement(BottomSheetKeyboardAwareScrollView, { ref: scrollViewRef, contentContainerStyle: { paddingBottom: insets.bottom }, keyboardShouldPersistTaps: keyboardShouldPersistTaps ? "handled" : "never", showsVerticalScrollIndicator: false, onScroll: handleOnScroll, stickyHeaderIndices: [0], bottomOffset: KEYBOARD_TOP_PADDING_AUTO_SCROLL },
175
+ renderHeader(),
176
+ React.createElement(View, { testID: "ATL-Overlay-Children" }, children))) : (React.createElement(BottomSheetView, null,
177
+ renderHeader(),
178
+ React.createElement(View, { style: { paddingBottom: insets.bottom }, testID: "ATL-Overlay-Children" }, children))))));
144
179
  }
145
- function ContentOverlayPortal(modalProps, ref) {
146
- return (React.createElement(Portal, null,
147
- React.createElement(ContentOverlayModal, Object.assign({ ref: ref }, modalProps))));
180
+ function Backdrop(bottomSheetBackdropProps) {
181
+ const styles = useStyles();
182
+ const { pressBehavior } = bottomSheetBackdropProps, props = __rest(bottomSheetBackdropProps, ["pressBehavior"]);
183
+ return (React.createElement(BottomSheetBackdrop, Object.assign({}, props, { appearsOnIndex: 0, disappearsOnIndex: -1, style: styles.backdrop, opacity: 1, pressBehavior: pressBehavior })));
148
184
  }
@@ -1,36 +1,32 @@
1
1
  import { buildThemedStyles } from "../AtlantisThemeContext";
2
2
  export const useStyles = buildThemedStyles(tokens => {
3
3
  const modalBorderRadius = tokens["radius-larger"];
4
- const titleOffsetFromHandle = tokens["space-base"];
5
4
  return {
5
+ handleWrapper: {
6
+ paddingBottom: tokens["space-smallest"],
7
+ paddingTop: tokens["space-small"],
8
+ },
6
9
  handle: {
7
10
  width: tokens["space-largest"],
8
11
  height: tokens["space-smaller"] + tokens["space-smallest"],
9
12
  backgroundColor: tokens["color-border"],
10
- top: tokens["space-small"],
11
13
  borderRadius: tokens["radius-circle"],
12
14
  },
13
- overlay: {
15
+ backdrop: {
14
16
  backgroundColor: tokens["color-overlay"],
15
17
  },
16
- modal: {
18
+ background: {
17
19
  borderTopLeftRadius: modalBorderRadius,
18
20
  borderTopRightRadius: modalBorderRadius,
19
21
  },
20
- modalForLargeScreens: {
21
- width: 640,
22
- alignSelf: "center",
23
- },
24
22
  header: {
25
23
  flexDirection: "row",
26
- backgroundColor: tokens["color-surface"],
27
- paddingTop: titleOffsetFromHandle,
28
24
  zIndex: tokens["elevation-base"],
25
+ minHeight: tokens["space-extravagant"] - tokens["space-base"],
29
26
  borderTopLeftRadius: modalBorderRadius,
30
27
  borderTopRightRadius: modalBorderRadius,
31
- minHeight: tokens["space-extravagant"],
32
28
  },
33
- headerShadow: Object.assign({}, tokens["shadow-base"]),
29
+ headerShadow: Object.assign(Object.assign({}, tokens["shadow-base"]), { position: "absolute", top: -20, height: 20, width: "100%" }),
34
30
  childrenStyle: {
35
31
  // We need to explicity lower the zIndex because otherwise, the modal content slides over the header shadow.
36
32
  zIndex: -1,
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Computes the abstract behavior of ContentOverlay from its props and state.
3
+ *
4
+ * This pure function documents and centralizes the complex logic that determines:
5
+ * - Initial height mode (fullScreen vs contentHeight)
6
+ * - Whether the overlay is draggable
7
+ * - Whether the dismiss button should be shown
8
+ *
9
+ * The logic accounts for legacy behavior where:
10
+ * - `onBeforeExit` silently overrides `isDraggable` to false
11
+ * - Default props (neither fullScreen nor adjustToContentHeight) are treated
12
+ * as contentHeight for the new implementation
13
+ * - Dismiss button visibility depends on multiple factors including position state
14
+ */
15
+ export function computeContentOverlayBehavior(config, state) {
16
+ const isDraggable = computeIsDraggable(config);
17
+ const initialHeight = computeInitialHeight(config, isDraggable);
18
+ const showDismiss = computeShowDismiss(config, state);
19
+ return {
20
+ initialHeight,
21
+ isDraggable,
22
+ showDismiss,
23
+ };
24
+ }
25
+ /**
26
+ * Order is important to maintain legacy behavior, despite the questionable logic.
27
+ * A non draggable overlay wants to be fullscreen, so as to have the dismiss button be visible.
28
+ * There is an invalid combination here with adjustToContentHeight and onBeforeExit which in turn overrides isDraggable to false.
29
+ * This requires an explicit showDismiss=true or else it will not be possible to dismiss the overlay.
30
+ */
31
+ function computeInitialHeight(config, isDraggable) {
32
+ if (config.adjustToContentHeight) {
33
+ return "contentHeight";
34
+ }
35
+ if (config.fullScreen) {
36
+ return "fullScreen";
37
+ }
38
+ if (!isDraggable) {
39
+ return "fullScreen";
40
+ }
41
+ return "contentHeight";
42
+ }
43
+ /**
44
+ * Draggability determination:
45
+ * - hasOnBeforeExit: true → false (silent override, regardless of isDraggable prop)
46
+ * - Otherwise → use isDraggable prop value
47
+ *
48
+ * This silent override exists because onBeforeExit needs to intercept close attempts,
49
+ * and dragging would bypass that interception.
50
+ */
51
+ function computeIsDraggable(config) {
52
+ if (config.hasOnBeforeExit) {
53
+ return false;
54
+ }
55
+ return config.isDraggable;
56
+ }
57
+ /**
58
+ * Dismiss button visibility:
59
+ * The idea behind fullscreen having it is that there may be little room to tap the background to dismiss.
60
+ * While this logic is redundant with the position, it's a relic of the legacy behavior where position didn't update in time.
61
+ */
62
+ function computeShowDismiss(config, state) {
63
+ if (config.showDismiss) {
64
+ return true;
65
+ }
66
+ if (state.isScreenReaderEnabled) {
67
+ return true;
68
+ }
69
+ if (config.fullScreen) {
70
+ return true;
71
+ }
72
+ if (!config.adjustToContentHeight && state.position === 0) {
73
+ return true;
74
+ }
75
+ return false;
76
+ }
@@ -0,0 +1 @@
1
+ export const KEYBOARD_TOP_PADDING_AUTO_SCROLL = 20;
@@ -0,0 +1,25 @@
1
+ import { useCallback, useRef } from "react";
2
+ import { BackHandler } from "react-native";
3
+ /**
4
+ * Hook that dismisses the bottom sheet on the hardware back button press if it is visible
5
+ * @param bottomSheetModalRef ref to the bottom sheet modal component
6
+ */
7
+ export function useBottomSheetModalBackHandler(onCloseController) {
8
+ const backHandlerSubscriptionRef = useRef(null);
9
+ const handleSheetPositionChange = useCallback((index) => {
10
+ var _a;
11
+ const isBottomSheetModalVisible = index >= 0;
12
+ if (isBottomSheetModalVisible && !backHandlerSubscriptionRef.current) {
13
+ // Setup the back handler if the bottom sheet is right in front of the user
14
+ backHandlerSubscriptionRef.current = BackHandler.addEventListener("hardwareBackPress", () => {
15
+ onCloseController();
16
+ return true;
17
+ });
18
+ }
19
+ else if (!isBottomSheetModalVisible) {
20
+ (_a = backHandlerSubscriptionRef.current) === null || _a === void 0 ? void 0 : _a.remove();
21
+ backHandlerSubscriptionRef.current = null;
22
+ }
23
+ }, [onCloseController]);
24
+ return { handleSheetPositionChange };
25
+ }
@@ -1 +1 @@
1
- export { ContentOverlay } from "./ContentOverlay";
1
+ export { ContentOverlay, useIsKeyboardHandledByScrollView, } from "./ContentOverlay";