@jobber/components-native 0.95.3 → 0.95.4-improve-co-ca924fd.14

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 (37) hide show
  1. package/dist/package.json +3 -5
  2. package/dist/src/ContentOverlay/ContentOverlay.js +128 -107
  3. package/dist/src/ContentOverlay/ContentOverlay.style.js +8 -12
  4. package/dist/src/ContentOverlay/ContentOverlayProvider.js +5 -0
  5. package/dist/src/ContentOverlay/computeContentOverlayBehavior.js +76 -0
  6. package/dist/src/ContentOverlay/hooks/useBottomSheetModalBackHandler.js +25 -0
  7. package/dist/src/ContentOverlay/index.js +1 -0
  8. package/dist/src/InputText/InputText.js +35 -1
  9. package/dist/src/utils/meta/meta.json +1 -0
  10. package/dist/tsconfig.build.tsbuildinfo +1 -1
  11. package/dist/types/src/ContentOverlay/ContentOverlay.d.ts +1 -5
  12. package/dist/types/src/ContentOverlay/ContentOverlay.style.d.ts +11 -10
  13. package/dist/types/src/ContentOverlay/ContentOverlayProvider.d.ts +6 -0
  14. package/dist/types/src/ContentOverlay/computeContentOverlayBehavior.d.ts +32 -0
  15. package/dist/types/src/ContentOverlay/hooks/useBottomSheetModalBackHandler.d.ts +7 -0
  16. package/dist/types/src/ContentOverlay/index.d.ts +1 -0
  17. package/dist/types/src/ContentOverlay/types.d.ts +5 -12
  18. package/jestSetup.js +2 -0
  19. package/package.json +3 -5
  20. package/src/ContentOverlay/ContentOverlay.stories.tsx +59 -0
  21. package/src/ContentOverlay/ContentOverlay.style.ts +12 -12
  22. package/src/ContentOverlay/ContentOverlay.test.tsx +157 -79
  23. package/src/ContentOverlay/ContentOverlay.tsx +223 -210
  24. package/src/ContentOverlay/ContentOverlayProvider.tsx +12 -0
  25. package/src/ContentOverlay/computeContentOverlayBehavior.test.ts +276 -0
  26. package/src/ContentOverlay/computeContentOverlayBehavior.ts +119 -0
  27. package/src/ContentOverlay/hooks/useBottomSheetModalBackHandler.test.ts +81 -0
  28. package/src/ContentOverlay/hooks/useBottomSheetModalBackHandler.ts +36 -0
  29. package/src/ContentOverlay/index.ts +1 -0
  30. package/src/ContentOverlay/types.ts +5 -13
  31. package/src/InputText/InputText.test.tsx +122 -0
  32. package/src/InputText/InputText.tsx +52 -2
  33. package/src/ThumbnailList/__snapshots__/ThumbnailList.test.tsx.snap +0 -20
  34. package/src/utils/meta/meta.json +1 -0
  35. package/dist/src/ContentOverlay/UNSAFE_WrappedModalize.js +0 -23
  36. package/dist/types/src/ContentOverlay/UNSAFE_WrappedModalize.d.ts +0 -3
  37. 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.95.3",
3
+ "version": "0.95.4-improve-co-ca924fd.14+ca924fd5a",
4
4
  "license": "MIT",
5
5
  "description": "React Native implementation of Atlantis",
6
6
  "repository": {
@@ -44,9 +44,8 @@
44
44
  "deepmerge": "^4.2.2",
45
45
  "lodash": "^4.17.21",
46
46
  "react-hook-form": "^7.52.0",
47
- "react-intl": "^7.1.11",
47
+ "react-intl": "^6 || ^7",
48
48
  "react-native-keyboard-aware-scroll-view": "^0.9.5",
49
- "react-native-modalize": "^2.0.13",
50
49
  "react-native-portalize": "^1.0.7",
51
50
  "react-native-toast-message": "^2.1.6",
52
51
  "react-native-uuid": "^1.4.9",
@@ -90,11 +89,10 @@
90
89
  "react-native-gesture-handler": ">=2.22.0",
91
90
  "react-native-keyboard-aware-scroll-view": "^0.9.5",
92
91
  "react-native-modal-datetime-picker": " >=13.0.0",
93
- "react-native-modalize": "^2.0.13",
94
92
  "react-native-portalize": "^1.0.7",
95
93
  "react-native-reanimated": "^3.0.0",
96
94
  "react-native-safe-area-context": "^5.4.0",
97
95
  "react-native-svg": ">=12.0.0"
98
96
  },
99
- "gitHead": "d082bc71718efe42e118b1f3adcd9007bb1923cb"
97
+ "gitHead": "ca924fd5a3a8d378218db75aa5b9233c46e7c256"
100
98
  }
@@ -1,121 +1,148 @@
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, { 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, BottomSheetScrollView, BottomSheetView, } from "@gorhom/bottom-sheet";
6
16
  import { useStyles } from "./ContentOverlay.style";
7
- import { useViewLayoutHeight } from "./hooks/useViewLayoutHeight";
8
- import { UNSAFE_WrappedModalize } from "./UNSAFE_WrappedModalize";
17
+ import { useBottomSheetModalBackHandler } from "./hooks/useBottomSheetModalBackHandler";
18
+ import { computeContentOverlayBehavior } from "./computeContentOverlayBehavior";
9
19
  import { useIsScreenReaderEnabled } from "../hooks";
10
20
  import { IconButton } from "../IconButton";
11
21
  import { Heading } from "../Heading";
12
22
  import { useAtlantisI18n } from "../hooks/useAtlantisI18n";
13
23
  import { useAtlantisTheme } from "../AtlantisThemeContext";
14
- export const ContentOverlay = forwardRef(ContentOverlayPortal);
15
- const ContentOverlayModal = forwardRef(ContentOverlayInternal);
24
+ const LARGE_SCREEN_BREAKPOINT = 640;
25
+ function getModalBackgroundColor(variation, tokens) {
26
+ switch (variation) {
27
+ case "surface":
28
+ return tokens["color-surface"];
29
+ case "background":
30
+ return tokens["color-surface--background"];
31
+ }
32
+ }
16
33
  // 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;
34
+ 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, }) {
35
+ const insets = useSafeAreaInsets();
36
+ const { width: windowWidth } = useWindowDimensions();
37
+ const bottomSheetModalRef = useRef(null);
38
+ const previousIndexRef = useRef(-1);
39
+ const [currentPosition, setCurrentPosition] = useState(-1);
40
+ const styles = useStyles();
20
41
  const { t } = useAtlantisI18n();
21
42
  const { tokens } = useAtlantisTheme();
22
- const { width: windowWidth, height: windowHeight } = useWindowDimensions();
23
- const insets = useSafeAreaInsets();
24
- const [position, setPosition] = useState("initial");
25
43
  const isScreenReaderEnabled = useIsScreenReaderEnabled();
26
- const isFullScreenOrTopPosition = fullScreen || (!adjustToContentHeight && position === "top");
27
- const shouldShowDismiss = showDismiss || isScreenReaderEnabled || isFullScreenOrTopPosition;
44
+ const behavior = computeContentOverlayBehavior({
45
+ fullScreen,
46
+ adjustToContentHeight,
47
+ isDraggable,
48
+ hasOnBeforeExit: onBeforeExit !== undefined,
49
+ showDismiss,
50
+ }, {
51
+ isScreenReaderEnabled,
52
+ position: currentPosition,
53
+ });
54
+ const effectiveIsDraggable = behavior.isDraggable;
55
+ const shouldShowDismiss = behavior.showDismiss;
56
+ const isCloseableOnOverlayTap = onBeforeExit === undefined;
57
+ // Prevent the Overlay from being flush with the top of the screen, even if we are "100%" or "fullscreen"
58
+ const topInset = insets.top || tokens["space-larger"];
28
59
  const [showHeaderShadow, setShowHeaderShadow] = useState(false);
29
60
  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
- }
61
+ const scrollViewRef = useRef(null);
62
+ // enableDynamicSizing will add another snap point of the content height
63
+ const snapPoints = useMemo(() => {
64
+ // There is a bug with "restore" behavior after keyboard is dismissed.
65
+ // https://github.com/gorhom/react-native-bottom-sheet/issues/2465
66
+ // providing a 100% snap point "fixes" it for now, but there is an approved PR to fix it
67
+ // that just needs to be merged and released: https://github.com/gorhom/react-native-bottom-sheet/pull/2511
68
+ return ["100%"];
37
69
  }, []);
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
70
  const onCloseController = () => {
78
71
  var _a;
79
72
  if (!onBeforeExit) {
80
- (_a = internalRef.current) === null || _a === void 0 ? void 0 : _a.close();
81
- return true;
73
+ (_a = bottomSheetModalRef.current) === null || _a === void 0 ? void 0 : _a.dismiss();
82
74
  }
83
75
  else {
84
76
  onBeforeExit();
85
- return false;
86
77
  }
87
78
  };
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
- }
79
+ const { handleSheetPositionChange } = useBottomSheetModalBackHandler(onCloseController);
80
+ useImperativeHandle(ref, () => ({
81
+ open: () => {
82
+ var _a;
83
+ (_a = bottomSheetModalRef.current) === null || _a === void 0 ? void 0 : _a.present();
84
+ },
85
+ close: () => {
86
+ var _a;
87
+ (_a = bottomSheetModalRef.current) === null || _a === void 0 ? void 0 : _a.dismiss();
88
+ },
89
+ }));
90
+ const handleChange = (index, position) => {
91
+ const previousIndex = previousIndexRef.current;
92
+ setCurrentPosition(position);
93
+ handleSheetPositionChange(index);
94
+ if (previousIndex === -1 && index >= 0) {
95
+ // Transitioned from closed to open
96
+ onOpen === null || onOpen === void 0 ? void 0 : onOpen();
97
+ // Set accessibility focus on header when opened
98
+ if (overlayHeader.current) {
99
+ const reactTag = findNodeHandle(overlayHeader.current);
100
+ if (reactTag) {
101
+ AccessibilityInfo.setAccessibilityFocus(reactTag);
95
102
  }
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() {
103
+ }
104
+ }
105
+ previousIndexRef.current = index;
106
+ };
107
+ const handleOnScroll = () => {
108
+ var _a;
109
+ const scrollTop = ((_a = scrollViewRef.current) === null || _a === void 0 ? void 0 : _a.scrollTop) || 0;
110
+ setShowHeaderShadow(scrollTop > 0);
111
+ };
112
+ const sheetStyle = useMemo(() => windowWidth > LARGE_SCREEN_BREAKPOINT
113
+ ? {
114
+ width: LARGE_SCREEN_BREAKPOINT,
115
+ marginLeft: (windowWidth - LARGE_SCREEN_BREAKPOINT) / 2,
116
+ }
117
+ : undefined, [windowWidth]);
118
+ const backgroundStyle = [
119
+ styles.background,
120
+ { backgroundColor: getModalBackgroundColor(modalBackgroundColor, tokens) },
121
+ ];
122
+ const handleIndicatorStyles = [
123
+ styles.handle,
124
+ !effectiveIsDraggable && {
125
+ opacity: 0,
126
+ },
127
+ ];
128
+ const renderHeader = () => {
110
129
  const closeOverlayA11YLabel = t("ContentOverlay.close", {
111
130
  title: title,
112
131
  });
113
132
  const headerStyles = [
114
133
  styles.header,
134
+ {
135
+ // Background color is necessary for scrollable modals as the content flows behind the header.
136
+ backgroundColor: getModalBackgroundColor(modalBackgroundColor, tokens),
137
+ },
138
+ ];
139
+ const headerShadowStyles = [
115
140
  showHeaderShadow && styles.headerShadow,
116
- { backgroundColor: getModalBackgroundColor(modalBackgroundColor) },
141
+ {
142
+ backgroundColor: getModalBackgroundColor(modalBackgroundColor, tokens),
143
+ },
117
144
  ];
118
- return (React.createElement(View, { onLayout: handleHeaderLayout, testID: "ATL-Overlay-Header" },
145
+ return (React.createElement(View, { testID: "ATL-Overlay-Header" },
119
146
  React.createElement(View, { style: headerStyles },
120
147
  React.createElement(View, { style: [
121
148
  styles.title,
@@ -125,24 +152,18 @@ function ContentOverlayInternal({ children, title, accessibilityLabel, fullScree
125
152
  ] },
126
153
  React.createElement(Heading, { level: "subtitle", variation: loading ? "subdued" : "heading", align: "start" }, title)),
127
154
  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
- }
155
+ React.createElement(IconButton, { name: "cross", customColor: loading ? tokens["color-disabled"] : tokens["color-heading"], onPress: () => onCloseController(), accessibilityLabel: closeOverlayA11YLabel, testID: "ATL-Overlay-CloseButton" })))),
156
+ React.createElement(View, null,
157
+ React.createElement(View, { style: headerShadowStyles }))));
158
+ };
159
+ 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() }, scrollEnabled ? (React.createElement(BottomSheetScrollView, { ref: scrollViewRef, contentContainerStyle: { paddingBottom: insets.bottom }, keyboardShouldPersistTaps: keyboardShouldPersistTaps ? "handled" : "never", showsVerticalScrollIndicator: false, onScroll: handleOnScroll, stickyHeaderIndices: [0] },
160
+ renderHeader(),
161
+ React.createElement(View, { testID: "ATL-Overlay-Children" }, children))) : (React.createElement(BottomSheetView, null,
162
+ renderHeader(),
163
+ React.createElement(View, { style: { paddingBottom: insets.bottom }, testID: "ATL-Overlay-Children" }, children)))));
144
164
  }
145
- function ContentOverlayPortal(modalProps, ref) {
146
- return (React.createElement(Portal, null,
147
- React.createElement(ContentOverlayModal, Object.assign({ ref: ref }, modalProps))));
165
+ function Backdrop(bottomSheetBackdropProps) {
166
+ const styles = useStyles();
167
+ const { pressBehavior } = bottomSheetBackdropProps, props = __rest(bottomSheetBackdropProps, ["pressBehavior"]);
168
+ return (React.createElement(BottomSheetBackdrop, Object.assign({}, props, { appearsOnIndex: 0, disappearsOnIndex: -1, style: styles.backdrop, opacity: 1, pressBehavior: pressBehavior })));
148
169
  }
@@ -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,5 @@
1
+ import React from "react";
2
+ import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
3
+ export function ContentOverlayProvider({ children, }) {
4
+ return React.createElement(BottomSheetModalProvider, null, children);
5
+ }
@@ -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,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,2 @@
1
1
  export { ContentOverlay } from "./ContentOverlay";
2
+ export { ContentOverlayProvider } from "./ContentOverlayProvider";
@@ -1,5 +1,6 @@
1
1
  import React, { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState, } from "react";
2
- import { Platform, TextInput } from "react-native";
2
+ import { Platform, TextInput, findNodeHandle } from "react-native";
3
+ import { useBottomSheetInternal } from "@gorhom/bottom-sheet";
3
4
  import identity from "lodash/identity";
4
5
  import { useShowClear } from "@jobber/hooks";
5
6
  import { useStyles } from "./InputText.style";
@@ -32,6 +33,10 @@ function InputTextInternal({ invalid, disabled, readonly = false, name, placehol
32
33
  hasValue,
33
34
  disabled,
34
35
  });
36
+ // Bottom sheet keyboard handling - detect if we're inside a ContentOverlay
37
+ const bottomSheetContext = useBottomSheetInternal(true);
38
+ const animatedKeyboardState = bottomSheetContext === null || bottomSheetContext === void 0 ? void 0 : bottomSheetContext.animatedKeyboardState;
39
+ const textInputNodesRef = bottomSheetContext === null || bottomSheetContext === void 0 ? void 0 : bottomSheetContext.textInputNodesRef;
35
40
  // Android doesn't have an accessibility label like iOS does. By adding
36
41
  // it as a placeholder it readds it like a label. However we don't want to
37
42
  // add a placeholder on iOS.
@@ -97,10 +102,12 @@ function InputTextInternal({ invalid, disabled, readonly = false, name, placehol
97
102
  // This is tech debt related to an issue where keyboard aware scrollview doesn't work if `scrollEnabled` is true. However,
98
103
  // when `scrollEnabled` is false it causes an issue where super long text inputs will jump to the top when a new line is added to the bottom of the input.
99
104
  scrollEnabled: Platform.OS === "ios" && multiline, textContentType: textContentType, onChangeText: handleChangeText, onSubmitEditing: handleOnSubmitEditing, returnKeyType: returnKeyType, blurOnSubmit: shouldBlurOnSubmit, accessibilityLabel: accessibilityLabel || placeholder, accessibilityHint: accessibilityHint, accessibilityState: { busy: loading }, secureTextEntry: secureTextEntry }, androidA11yProps, { onFocus: event => {
105
+ handleBottomSheetFocus(event);
100
106
  _name && setFocusedInput(_name);
101
107
  setFocused(true);
102
108
  onFocus === null || onFocus === void 0 ? void 0 : onFocus(event);
103
109
  }, onBlur: event => {
110
+ handleBottomSheetBlur(event);
104
111
  _name && setFocusedInput("");
105
112
  setFocused(false);
106
113
  onBlur === null || onBlur === void 0 ? void 0 : onBlur(event);
@@ -121,6 +128,33 @@ function InputTextInternal({ invalid, disabled, readonly = false, name, placehol
121
128
  const removedIOSCharValue = isIOS ? value.replace(/\uFFFC/g, "") : value;
122
129
  updateFormAndState(removedIOSCharValue);
123
130
  }
131
+ function handleBottomSheetFocus(event) {
132
+ if (!animatedKeyboardState || !textInputNodesRef || !(event === null || event === void 0 ? void 0 : event.nativeEvent)) {
133
+ return;
134
+ }
135
+ animatedKeyboardState.set(state => (Object.assign(Object.assign({}, state), { target: event.nativeEvent.target })));
136
+ }
137
+ function handleBottomSheetBlur(event) {
138
+ if (!animatedKeyboardState || !textInputNodesRef || !(event === null || event === void 0 ? void 0 : event.nativeEvent)) {
139
+ return;
140
+ }
141
+ const keyboardState = animatedKeyboardState.get();
142
+ const currentlyFocusedInput = TextInput.State.currentlyFocusedInput();
143
+ const currentFocusedInput = currentlyFocusedInput !== null
144
+ ? findNodeHandle(
145
+ // @ts-expect-error - TextInput.State.currentlyFocusedInput() returns NativeMethods
146
+ // which is not directly assignable to findNodeHandle's expected type,
147
+ // but it works at runtime. This is a known type limitation in React Native.
148
+ currentlyFocusedInput)
149
+ : null;
150
+ // Only remove the target if it belongs to the current component
151
+ // and if the currently focused input is not in the targets set
152
+ const shouldRemoveCurrentTarget = keyboardState.target === event.nativeEvent.target;
153
+ const shouldIgnoreBlurEvent = currentFocusedInput && textInputNodesRef.current.has(currentFocusedInput);
154
+ if (shouldRemoveCurrentTarget && !shouldIgnoreBlurEvent) {
155
+ animatedKeyboardState.set(state => (Object.assign(Object.assign({}, state), { target: undefined })));
156
+ }
157
+ }
124
158
  function handleClear() {
125
159
  handleChangeText("");
126
160
  }
@@ -23,6 +23,7 @@
23
23
  "Chip",
24
24
  "Content",
25
25
  "ContentOverlay",
26
+ "ContentOverlayProvider",
26
27
  "Disclosure",
27
28
  "Divider",
28
29
  "EmptyState",