@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.
- package/dist/package.json +3 -5
- package/dist/src/ContentOverlay/ContentOverlay.js +128 -107
- package/dist/src/ContentOverlay/ContentOverlay.style.js +8 -12
- package/dist/src/ContentOverlay/ContentOverlayProvider.js +5 -0
- package/dist/src/ContentOverlay/computeContentOverlayBehavior.js +76 -0
- package/dist/src/ContentOverlay/hooks/useBottomSheetModalBackHandler.js +25 -0
- package/dist/src/ContentOverlay/index.js +1 -0
- package/dist/src/InputText/InputText.js +35 -1
- package/dist/src/utils/meta/meta.json +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/dist/types/src/ContentOverlay/ContentOverlay.d.ts +1 -5
- package/dist/types/src/ContentOverlay/ContentOverlay.style.d.ts +11 -10
- package/dist/types/src/ContentOverlay/ContentOverlayProvider.d.ts +6 -0
- package/dist/types/src/ContentOverlay/computeContentOverlayBehavior.d.ts +32 -0
- package/dist/types/src/ContentOverlay/hooks/useBottomSheetModalBackHandler.d.ts +7 -0
- package/dist/types/src/ContentOverlay/index.d.ts +1 -0
- package/dist/types/src/ContentOverlay/types.d.ts +5 -12
- package/jestSetup.js +2 -0
- package/package.json +3 -5
- package/src/ContentOverlay/ContentOverlay.stories.tsx +59 -0
- package/src/ContentOverlay/ContentOverlay.style.ts +12 -12
- package/src/ContentOverlay/ContentOverlay.test.tsx +157 -79
- package/src/ContentOverlay/ContentOverlay.tsx +223 -210
- package/src/ContentOverlay/ContentOverlayProvider.tsx +12 -0
- package/src/ContentOverlay/computeContentOverlayBehavior.test.ts +276 -0
- package/src/ContentOverlay/computeContentOverlayBehavior.ts +119 -0
- package/src/ContentOverlay/hooks/useBottomSheetModalBackHandler.test.ts +81 -0
- package/src/ContentOverlay/hooks/useBottomSheetModalBackHandler.ts +36 -0
- package/src/ContentOverlay/index.ts +1 -0
- package/src/ContentOverlay/types.ts +5 -13
- package/src/InputText/InputText.test.tsx +122 -0
- package/src/InputText/InputText.tsx +52 -2
- package/src/ThumbnailList/__snapshots__/ThumbnailList.test.tsx.snap +0 -20
- package/src/utils/meta/meta.json +1 -0
- package/dist/src/ContentOverlay/UNSAFE_WrappedModalize.js +0 -23
- package/dist/types/src/ContentOverlay/UNSAFE_WrappedModalize.d.ts +0 -3
- 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
|
+
"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
|
|
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": "
|
|
97
|
+
"gitHead": "ca924fd5a3a8d378218db75aa5b9233c46e7c256"
|
|
100
98
|
}
|
|
@@ -1,121 +1,148 @@
|
|
|
1
|
-
|
|
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 {
|
|
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 {
|
|
8
|
-
import {
|
|
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
|
-
|
|
15
|
-
|
|
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
|
|
18
|
-
|
|
19
|
-
const
|
|
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
|
|
27
|
-
|
|
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
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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 =
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
{
|
|
141
|
+
{
|
|
142
|
+
backgroundColor: getModalBackgroundColor(modalBackgroundColor, tokens),
|
|
143
|
+
},
|
|
117
144
|
];
|
|
118
|
-
return (React.createElement(View, {
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
15
|
+
backdrop: {
|
|
14
16
|
backgroundColor: tokens["color-overlay"],
|
|
15
17
|
},
|
|
16
|
-
|
|
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,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,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
|
}
|