@jobber/components-native 0.98.5 → 0.100.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/package.json +3 -6
- package/dist/src/AtlantisOverlayProvider/AtlantisOverlayProvider.js +5 -0
- package/dist/src/AtlantisOverlayProvider/index.js +1 -0
- package/dist/src/BottomSheet/BottomSheet.js +9 -11
- package/dist/src/BottomSheet/hooks/useBottomSheetBackHandler.js +2 -2
- package/dist/src/ButtonGroup/components/SecondaryActionSheet/SecondaryActionSheet.js +9 -11
- 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/FormatFile/components/FormatFileBottomSheet/FormatFileBottomSheet.js +7 -9
- package/dist/src/InputText/InputText.js +44 -1
- package/dist/src/index.js +1 -0
- package/dist/src/utils/meta/meta.json +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/dist/types/src/AtlantisOverlayProvider/AtlantisOverlayProvider.d.ts +6 -0
- package/dist/types/src/AtlantisOverlayProvider/index.d.ts +1 -0
- package/dist/types/src/BottomSheet/hooks/useBottomSheetBackHandler.d.ts +3 -3
- 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/dist/types/src/index.d.ts +1 -0
- package/jestSetup.js +2 -0
- package/package.json +3 -6
- package/src/AtlantisOverlayProvider/AtlantisOverlayProvider.tsx +12 -0
- package/src/AtlantisOverlayProvider/index.ts +1 -0
- package/src/BottomSheet/BottomSheet.tsx +13 -13
- package/src/BottomSheet/hooks/useBottomSheetBackHandler.test.ts +10 -10
- package/src/BottomSheet/hooks/useBottomSheetBackHandler.ts +4 -4
- package/src/ButtonGroup/ButtonGroup.stories.tsx +10 -8
- package/src/ButtonGroup/ButtonGroup.test.tsx +7 -10
- package/src/ButtonGroup/components/SecondaryActionSheet/SecondaryActionSheet.tsx +26 -29
- 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/Form/Form.stories.tsx +8 -4
- package/src/Form/Form.test.tsx +51 -54
- package/src/Form/components/FormSaveButton/FormSaveButton.test.tsx +7 -10
- package/src/FormatFile/FormatFile.stories.tsx +3 -4
- package/src/FormatFile/FormatFile.test.tsx +11 -14
- package/src/FormatFile/components/FormatFileBottomSheet/FormatFileBottomSheet.test.tsx +6 -9
- package/src/FormatFile/components/FormatFileBottomSheet/FormatFileBottomSheet.tsx +21 -24
- package/src/InputDate/InputDate.test.tsx +5 -8
- package/src/InputText/InputText.test.tsx +122 -0
- package/src/InputText/InputText.tsx +62 -2
- package/src/InputTime/InputTime.stories.tsx +8 -4
- package/src/InputTime/InputTime.test.tsx +5 -8
- package/src/ThumbnailList/ThumbnailList.stories.tsx +6 -4
- package/src/ThumbnailList/ThumbnailList.test.tsx +5 -8
- package/src/ThumbnailList/__snapshots__/ThumbnailList.test.tsx.snap +101 -150
- package/src/index.ts +1 -0
- package/src/utils/meta/meta.json +2 -1
- 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.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "React Native implementation of Atlantis",
|
|
6
6
|
"repository": {
|
|
@@ -46,8 +46,6 @@
|
|
|
46
46
|
"react-hook-form": "^7.52.0",
|
|
47
47
|
"react-intl": "^7.1.11",
|
|
48
48
|
"react-native-keyboard-controller": "^1.20.7",
|
|
49
|
-
"react-native-modalize": "^2.0.13",
|
|
50
|
-
"react-native-portalize": "^1.0.7",
|
|
51
49
|
"react-native-toast-message": "^2.1.6",
|
|
52
50
|
"react-native-uuid": "^1.4.9",
|
|
53
51
|
"ts-xor": "^1.1.0"
|
|
@@ -69,6 +67,7 @@
|
|
|
69
67
|
"date-fns-tz": "^2.0.0",
|
|
70
68
|
"react-native": "^0.82.1",
|
|
71
69
|
"react-native-gesture-handler": "^2.29.1",
|
|
70
|
+
"react-native-keyboard-controller": "^1.12.0",
|
|
72
71
|
"react-native-modal-datetime-picker": "^18.0.0",
|
|
73
72
|
"react-native-reanimated": "^3.7.1",
|
|
74
73
|
"react-native-safe-area-context": "^5.4.0",
|
|
@@ -90,11 +89,9 @@
|
|
|
90
89
|
"react-native-gesture-handler": ">=2.22.0",
|
|
91
90
|
"react-native-keyboard-controller": "^1.20.7",
|
|
92
91
|
"react-native-modal-datetime-picker": " >=13.0.0",
|
|
93
|
-
"react-native-modalize": "^2.0.13",
|
|
94
|
-
"react-native-portalize": "^1.0.7",
|
|
95
92
|
"react-native-reanimated": "^3.0.0",
|
|
96
93
|
"react-native-safe-area-context": "^5.4.0",
|
|
97
94
|
"react-native-svg": ">=12.0.0"
|
|
98
95
|
},
|
|
99
|
-
"gitHead": "
|
|
96
|
+
"gitHead": "2ef76c22620e1627eca8cf576b35e78228262574"
|
|
100
97
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { AtlantisOverlayProvider } from "./AtlantisOverlayProvider";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React, { useCallback, useImperativeHandle, useRef } from "react";
|
|
2
2
|
import { Keyboard, View } from "react-native";
|
|
3
3
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
4
|
-
import
|
|
4
|
+
import { BottomSheetBackdrop, BottomSheetModal, BottomSheetView, } from "@gorhom/bottom-sheet";
|
|
5
5
|
import { tokens } from "@jobber/design";
|
|
6
6
|
import { useStyles } from "./BottomSheet.style";
|
|
7
7
|
import { BottomSheetOption } from "./components/BottomSheetOption";
|
|
@@ -22,7 +22,8 @@ export function BottomSheet({ children, showCancel, loading = false, heading, on
|
|
|
22
22
|
useImperativeHandle(ref, () => ({
|
|
23
23
|
open: () => {
|
|
24
24
|
var _a;
|
|
25
|
-
(
|
|
25
|
+
dismissKeyboard();
|
|
26
|
+
(_a = bottomSheetRef.current) === null || _a === void 0 ? void 0 : _a.present();
|
|
26
27
|
},
|
|
27
28
|
close: () => {
|
|
28
29
|
close();
|
|
@@ -30,25 +31,22 @@ export function BottomSheet({ children, showCancel, loading = false, heading, on
|
|
|
30
31
|
}));
|
|
31
32
|
const close = useCallback(() => {
|
|
32
33
|
var _a;
|
|
33
|
-
(_a = bottomSheetRef.current) === null || _a === void 0 ? void 0 : _a.
|
|
34
|
+
(_a = bottomSheetRef.current) === null || _a === void 0 ? void 0 : _a.dismiss();
|
|
34
35
|
}, []);
|
|
35
36
|
const handleChange = (index) => {
|
|
36
37
|
// Handle Android back button
|
|
37
38
|
handleSheetPositionChange(index);
|
|
38
39
|
const previousIndex = previousIndexRef.current;
|
|
39
40
|
if (previousIndex === -1 && index >= 0) {
|
|
40
|
-
// Transitioned from closed to open
|
|
41
|
-
dismissKeyboard();
|
|
42
41
|
onOpen === null || onOpen === void 0 ? void 0 : onOpen();
|
|
43
42
|
}
|
|
44
|
-
else if (previousIndex >= 0 && index === -1) {
|
|
45
|
-
// Transitioned from open to closed
|
|
46
|
-
dismissKeyboard();
|
|
47
|
-
onClose === null || onClose === void 0 ? void 0 : onClose();
|
|
48
|
-
}
|
|
49
43
|
previousIndexRef.current = index;
|
|
50
44
|
};
|
|
51
|
-
return (React.createElement(
|
|
45
|
+
return (React.createElement(BottomSheetModal, { ref: bottomSheetRef, backdropComponent: Backdrop, backgroundStyle: styles.background, enablePanDownToClose: true, onChange: handleChange, onDismiss: () => {
|
|
46
|
+
previousIndexRef.current = -1;
|
|
47
|
+
dismissKeyboard();
|
|
48
|
+
onClose === null || onClose === void 0 ? void 0 : onClose();
|
|
49
|
+
}, keyboardBlurBehavior: "restore", handleStyle: styles.handle },
|
|
52
50
|
React.createElement(BottomSheetView, { style: {
|
|
53
51
|
paddingBottom: insets.bottom + tokens["space-small"],
|
|
54
52
|
paddingTop: tokens["space-small"],
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useCallback, useRef } from "react";
|
|
2
2
|
import { BackHandler } from "react-native";
|
|
3
3
|
/**
|
|
4
|
-
* Hook that
|
|
4
|
+
* Hook that dismisses the bottom sheet modal on the hardware back button press if it is visible
|
|
5
5
|
* @param bottomSheetRef ref to the bottom sheet component
|
|
6
6
|
*/
|
|
7
7
|
export function useBottomSheetBackHandler(bottomSheetRef) {
|
|
@@ -13,7 +13,7 @@ export function useBottomSheetBackHandler(bottomSheetRef) {
|
|
|
13
13
|
// Setup the back handler if the bottom sheet is right in front of the user
|
|
14
14
|
backHandlerSubscriptionRef.current = BackHandler.addEventListener("hardwareBackPress", () => {
|
|
15
15
|
var _a;
|
|
16
|
-
(_a = bottomSheetRef.current) === null || _a === void 0 ? void 0 : _a.
|
|
16
|
+
(_a = bottomSheetRef.current) === null || _a === void 0 ? void 0 : _a.dismiss();
|
|
17
17
|
return true;
|
|
18
18
|
});
|
|
19
19
|
}
|
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { View } from "react-native";
|
|
3
|
-
import { Portal } from "react-native-portalize";
|
|
4
3
|
import { BottomSheetOption } from "../../../BottomSheet/components/BottomSheetOption";
|
|
5
4
|
import { BottomSheet } from "../../../BottomSheet/BottomSheet";
|
|
6
5
|
export function SecondaryActionSheet({ actions, secondaryActionsRef, heading, showCancel, onOpenBottomSheet, onCloseBottomSheet, }) {
|
|
7
|
-
return (React.createElement(
|
|
8
|
-
React.createElement(
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
})))));
|
|
6
|
+
return (React.createElement(BottomSheet, { heading: heading, showCancel: showCancel, ref: secondaryActionsRef, onOpen: onOpenBottomSheet, onClose: onCloseBottomSheet },
|
|
7
|
+
React.createElement(View, null, actions.map((action, index) => {
|
|
8
|
+
const { label, onPress, icon, iconColor, destructive } = action;
|
|
9
|
+
return (React.createElement(BottomSheetOption, { destructive: destructive, key: index, text: label, onPress: () => {
|
|
10
|
+
var _a;
|
|
11
|
+
(_a = secondaryActionsRef === null || secondaryActionsRef === void 0 ? void 0 : secondaryActionsRef.current) === null || _a === void 0 ? void 0 : _a.close();
|
|
12
|
+
onPress();
|
|
13
|
+
}, icon: icon, iconColor: iconColor }));
|
|
14
|
+
}))));
|
|
17
15
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { memo } from "react";
|
|
2
|
+
import { KeyboardAwareScrollView } from "react-native-keyboard-controller";
|
|
3
|
+
import { SCROLLABLE_TYPE, createBottomSheetScrollableComponent, } from "@gorhom/bottom-sheet";
|
|
4
|
+
import Reanimated from "react-native-reanimated";
|
|
5
|
+
/**
|
|
6
|
+
* A keyboard-aware scroll view component that integrates with @gorhom/bottom-sheet.
|
|
7
|
+
*
|
|
8
|
+
* This component wraps `KeyboardAwareScrollView` from `react-native-keyboard-controller`
|
|
9
|
+
* with the bottom sheet HOCs to ensure proper keyboard handling on Android when using
|
|
10
|
+
* TextInputs inside a BottomSheet.
|
|
11
|
+
*
|
|
12
|
+
* @see https://kirillzyusko.github.io/react-native-keyboard-controller/docs/api/components/keyboard-aware-scroll-view#gorhombottom-sheet
|
|
13
|
+
*/
|
|
14
|
+
const AnimatedScrollView = Reanimated.createAnimatedComponent(KeyboardAwareScrollView);
|
|
15
|
+
const BottomSheetScrollViewComponent = createBottomSheetScrollableComponent(SCROLLABLE_TYPE.SCROLLVIEW, AnimatedScrollView);
|
|
16
|
+
const BottomSheetKeyboardAwareScrollView = memo(BottomSheetScrollViewComponent);
|
|
17
|
+
BottomSheetKeyboardAwareScrollView.displayName =
|
|
18
|
+
"BottomSheetKeyboardAwareScrollView";
|
|
19
|
+
export { BottomSheetKeyboardAwareScrollView };
|
|
@@ -1,121 +1,162 @@
|
|
|
1
|
-
|
|
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";
|