@jobber/components-native 0.99.0 → 0.100.1
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 +4 -6
- package/dist/src/Button/Button.js +2 -2
- package/dist/src/ContentOverlay/BottomSheetKeyboardAwareScrollView.js +19 -0
- package/dist/src/ContentOverlay/ContentOverlay.js +143 -107
- package/dist/src/ContentOverlay/ContentOverlay.style.js +8 -12
- package/dist/src/ContentOverlay/computeContentOverlayBehavior.js +76 -0
- package/dist/src/ContentOverlay/constants.js +1 -0
- package/dist/src/ContentOverlay/hooks/useBottomSheetModalBackHandler.js +25 -0
- package/dist/src/ContentOverlay/index.js +1 -1
- package/dist/src/InputText/InputText.js +44 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/dist/types/src/ActionLabel/ActionLabel.d.ts +1 -1
- package/dist/types/src/ContentOverlay/BottomSheetKeyboardAwareScrollView.d.ts +11 -0
- package/dist/types/src/ContentOverlay/ContentOverlay.d.ts +2 -5
- package/dist/types/src/ContentOverlay/ContentOverlay.style.d.ts +11 -10
- package/dist/types/src/ContentOverlay/computeContentOverlayBehavior.d.ts +32 -0
- package/dist/types/src/ContentOverlay/constants.d.ts +1 -0
- package/dist/types/src/ContentOverlay/hooks/useBottomSheetModalBackHandler.d.ts +7 -0
- package/dist/types/src/ContentOverlay/index.d.ts +1 -1
- package/dist/types/src/ContentOverlay/types.d.ts +5 -12
- package/jestSetup.js +2 -0
- package/package.json +4 -6
- package/src/ActionLabel/ActionLabel.test.tsx +13 -1
- package/src/ActionLabel/ActionLabel.tsx +6 -1
- package/src/Button/Button.tsx +2 -2
- package/src/ContentOverlay/BottomSheetKeyboardAwareScrollView.tsx +36 -0
- package/src/ContentOverlay/ContentOverlay.stories.tsx +32 -36
- package/src/ContentOverlay/ContentOverlay.style.ts +12 -12
- package/src/ContentOverlay/ContentOverlay.test.tsx +157 -79
- package/src/ContentOverlay/ContentOverlay.tsx +247 -205
- package/src/ContentOverlay/computeContentOverlayBehavior.test.ts +276 -0
- package/src/ContentOverlay/computeContentOverlayBehavior.ts +119 -0
- package/src/ContentOverlay/constants.ts +1 -0
- package/src/ContentOverlay/hooks/useBottomSheetModalBackHandler.test.ts +81 -0
- package/src/ContentOverlay/hooks/useBottomSheetModalBackHandler.ts +36 -0
- package/src/ContentOverlay/index.ts +4 -1
- package/src/ContentOverlay/types.ts +5 -13
- package/src/InputText/InputText.test.tsx +122 -0
- package/src/InputText/InputText.tsx +62 -2
- 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.
|
|
3
|
+
"version": "0.100.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "React Native implementation of Atlantis",
|
|
6
6
|
"repository": {
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"build": "npm run build:clean && npm run compile",
|
|
33
33
|
"bootstrap": "npm run build",
|
|
34
34
|
"prepack": "npm run build",
|
|
35
|
+
"watch": "tsc -p tsconfig.build.json --watch --preserveWatchOutput",
|
|
35
36
|
"compile": "tsc -p tsconfig.build.json",
|
|
36
37
|
"build:clean": "rm -rf ./dist",
|
|
37
38
|
"storybook": "storybook dev -p 6008 --disable-telemetry",
|
|
@@ -46,8 +47,6 @@
|
|
|
46
47
|
"react-hook-form": "^7.52.0",
|
|
47
48
|
"react-intl": "^7.1.11",
|
|
48
49
|
"react-native-keyboard-controller": "^1.20.7",
|
|
49
|
-
"react-native-modalize": "^2.0.13",
|
|
50
|
-
"react-native-portalize": "^1.0.7",
|
|
51
50
|
"react-native-toast-message": "^2.1.6",
|
|
52
51
|
"react-native-uuid": "^1.4.9",
|
|
53
52
|
"ts-xor": "^1.1.0"
|
|
@@ -69,6 +68,7 @@
|
|
|
69
68
|
"date-fns-tz": "^2.0.0",
|
|
70
69
|
"react-native": "^0.82.1",
|
|
71
70
|
"react-native-gesture-handler": "^2.29.1",
|
|
71
|
+
"react-native-keyboard-controller": "^1.12.0",
|
|
72
72
|
"react-native-modal-datetime-picker": "^18.0.0",
|
|
73
73
|
"react-native-reanimated": "^3.7.1",
|
|
74
74
|
"react-native-safe-area-context": "^5.4.0",
|
|
@@ -90,11 +90,9 @@
|
|
|
90
90
|
"react-native-gesture-handler": ">=2.22.0",
|
|
91
91
|
"react-native-keyboard-controller": "^1.20.7",
|
|
92
92
|
"react-native-modal-datetime-picker": " >=13.0.0",
|
|
93
|
-
"react-native-modalize": "^2.0.13",
|
|
94
|
-
"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": "952a8f59bf74f0fec04b870102aa1b23289b2517"
|
|
100
98
|
}
|
|
@@ -44,11 +44,11 @@ function getActionLabelVariation(variation, type) {
|
|
|
44
44
|
}
|
|
45
45
|
switch (variation) {
|
|
46
46
|
case "learning":
|
|
47
|
-
return "
|
|
47
|
+
return "interactiveSubtle";
|
|
48
48
|
case "destructive":
|
|
49
49
|
return "destructive";
|
|
50
50
|
case "cancel":
|
|
51
|
-
return "
|
|
51
|
+
return "interactiveSubtle";
|
|
52
52
|
default:
|
|
53
53
|
return "interactive";
|
|
54
54
|
}
|
|
@@ -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
|
-
|
|
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 {
|
|
4
|
-
import {
|
|
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 {
|
|
8
|
-
import {
|
|
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
|
-
|
|
15
|
-
|
|
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
|
|
18
|
-
|
|
19
|
-
const
|
|
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
|
|
27
|
-
|
|
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
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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 =
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
{
|
|
155
|
+
{
|
|
156
|
+
backgroundColor: getModalBackgroundColor(modalBackgroundColor, tokens),
|
|
157
|
+
},
|
|
117
158
|
];
|
|
118
|
-
return (React.createElement(View, {
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
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 @@
|
|
|
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";
|
|
@@ -1,9 +1,11 @@
|
|
|
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";
|
|
6
7
|
import { useInputAccessoriesContext } from "./context";
|
|
8
|
+
import { useIsKeyboardHandledByScrollView } from "../ContentOverlay";
|
|
7
9
|
import { useFormController } from "../hooks";
|
|
8
10
|
import { InputFieldWrapper } from "../InputFieldWrapper";
|
|
9
11
|
import { useCommonInputStyles } from "../InputFieldWrapper/CommonInputStyles.style";
|
|
@@ -32,6 +34,18 @@ function InputTextInternal({ invalid, disabled, readonly = false, name, placehol
|
|
|
32
34
|
hasValue,
|
|
33
35
|
disabled,
|
|
34
36
|
});
|
|
37
|
+
// When inside a scrollable ContentOverlay, keyboard offset is handled by
|
|
38
|
+
// KeyboardAwareScrollView. Registering with the bottom-sheet's keyboard
|
|
39
|
+
// state would cause double-counted spacing, so we skip it.
|
|
40
|
+
const isKeyboardHandledByScrollView = useIsKeyboardHandledByScrollView();
|
|
41
|
+
const bottomSheetContext = useBottomSheetInternal(true);
|
|
42
|
+
const shouldHandleBottomSheetKeyboard = bottomSheetContext !== null && !isKeyboardHandledByScrollView;
|
|
43
|
+
const animatedKeyboardState = shouldHandleBottomSheetKeyboard
|
|
44
|
+
? bottomSheetContext.animatedKeyboardState
|
|
45
|
+
: undefined;
|
|
46
|
+
const textInputNodesRef = shouldHandleBottomSheetKeyboard
|
|
47
|
+
? bottomSheetContext.textInputNodesRef
|
|
48
|
+
: undefined;
|
|
35
49
|
// Android doesn't have an accessibility label like iOS does. By adding
|
|
36
50
|
// it as a placeholder it readds it like a label. However we don't want to
|
|
37
51
|
// add a placeholder on iOS.
|
|
@@ -97,10 +111,12 @@ function InputTextInternal({ invalid, disabled, readonly = false, name, placehol
|
|
|
97
111
|
// This is tech debt related to an issue where keyboard aware scrollview doesn't work if `scrollEnabled` is true. However,
|
|
98
112
|
// 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
113
|
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 => {
|
|
114
|
+
handleBottomSheetFocus(event);
|
|
100
115
|
_name && setFocusedInput(_name);
|
|
101
116
|
setFocused(true);
|
|
102
117
|
onFocus === null || onFocus === void 0 ? void 0 : onFocus(event);
|
|
103
118
|
}, onBlur: event => {
|
|
119
|
+
handleBottomSheetBlur(event);
|
|
104
120
|
_name && setFocusedInput("");
|
|
105
121
|
setFocused(false);
|
|
106
122
|
onBlur === null || onBlur === void 0 ? void 0 : onBlur(event);
|
|
@@ -121,6 +137,33 @@ function InputTextInternal({ invalid, disabled, readonly = false, name, placehol
|
|
|
121
137
|
const removedIOSCharValue = isIOS ? value.replace(/\uFFFC/g, "") : value;
|
|
122
138
|
updateFormAndState(removedIOSCharValue);
|
|
123
139
|
}
|
|
140
|
+
function handleBottomSheetFocus(event) {
|
|
141
|
+
if (!animatedKeyboardState || !textInputNodesRef || !(event === null || event === void 0 ? void 0 : event.nativeEvent)) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
animatedKeyboardState.set(state => (Object.assign(Object.assign({}, state), { target: event.nativeEvent.target })));
|
|
145
|
+
}
|
|
146
|
+
function handleBottomSheetBlur(event) {
|
|
147
|
+
if (!animatedKeyboardState || !textInputNodesRef || !(event === null || event === void 0 ? void 0 : event.nativeEvent)) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const keyboardState = animatedKeyboardState.get();
|
|
151
|
+
const currentlyFocusedInput = TextInput.State.currentlyFocusedInput();
|
|
152
|
+
const currentFocusedInput = currentlyFocusedInput !== null
|
|
153
|
+
? findNodeHandle(
|
|
154
|
+
// @ts-expect-error - TextInput.State.currentlyFocusedInput() returns NativeMethods
|
|
155
|
+
// which is not directly assignable to findNodeHandle's expected type,
|
|
156
|
+
// but it works at runtime. This is a known type limitation in React Native.
|
|
157
|
+
currentlyFocusedInput)
|
|
158
|
+
: null;
|
|
159
|
+
// Only remove the target if it belongs to the current component
|
|
160
|
+
// and if the currently focused input is not in the targets set
|
|
161
|
+
const shouldRemoveCurrentTarget = keyboardState.target === event.nativeEvent.target;
|
|
162
|
+
const shouldIgnoreBlurEvent = currentFocusedInput && textInputNodesRef.current.has(currentFocusedInput);
|
|
163
|
+
if (shouldRemoveCurrentTarget && !shouldIgnoreBlurEvent) {
|
|
164
|
+
animatedKeyboardState.set(state => (Object.assign(Object.assign({}, state), { target: undefined })));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
124
167
|
function handleClear() {
|
|
125
168
|
handleChangeText("");
|
|
126
169
|
}
|