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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/package.json +3 -5
  2. package/dist/src/ContentOverlay/ContentOverlay.js +128 -107
  3. package/dist/src/ContentOverlay/ContentOverlay.style.js +8 -12
  4. package/dist/src/ContentOverlay/ContentOverlayProvider.js +5 -0
  5. package/dist/src/ContentOverlay/computeContentOverlayBehavior.js +76 -0
  6. package/dist/src/ContentOverlay/hooks/useBottomSheetModalBackHandler.js +25 -0
  7. package/dist/src/ContentOverlay/index.js +1 -0
  8. package/dist/src/InputText/InputText.js +35 -1
  9. package/dist/src/utils/meta/meta.json +1 -0
  10. package/dist/tsconfig.build.tsbuildinfo +1 -1
  11. package/dist/types/src/ContentOverlay/ContentOverlay.d.ts +1 -5
  12. package/dist/types/src/ContentOverlay/ContentOverlay.style.d.ts +11 -10
  13. package/dist/types/src/ContentOverlay/ContentOverlayProvider.d.ts +6 -0
  14. package/dist/types/src/ContentOverlay/computeContentOverlayBehavior.d.ts +32 -0
  15. package/dist/types/src/ContentOverlay/hooks/useBottomSheetModalBackHandler.d.ts +7 -0
  16. package/dist/types/src/ContentOverlay/index.d.ts +1 -0
  17. package/dist/types/src/ContentOverlay/types.d.ts +5 -12
  18. package/jestSetup.js +2 -0
  19. package/package.json +3 -5
  20. package/src/ContentOverlay/ContentOverlay.stories.tsx +59 -0
  21. package/src/ContentOverlay/ContentOverlay.style.ts +12 -12
  22. package/src/ContentOverlay/ContentOverlay.test.tsx +157 -79
  23. package/src/ContentOverlay/ContentOverlay.tsx +223 -210
  24. package/src/ContentOverlay/ContentOverlayProvider.tsx +12 -0
  25. package/src/ContentOverlay/computeContentOverlayBehavior.test.ts +276 -0
  26. package/src/ContentOverlay/computeContentOverlayBehavior.ts +119 -0
  27. package/src/ContentOverlay/hooks/useBottomSheetModalBackHandler.test.ts +81 -0
  28. package/src/ContentOverlay/hooks/useBottomSheetModalBackHandler.ts +36 -0
  29. package/src/ContentOverlay/index.ts +1 -0
  30. package/src/ContentOverlay/types.ts +5 -13
  31. package/src/InputText/InputText.test.tsx +122 -0
  32. package/src/InputText/InputText.tsx +52 -2
  33. package/src/ThumbnailList/__snapshots__/ThumbnailList.test.tsx.snap +0 -20
  34. package/src/utils/meta/meta.json +1 -0
  35. package/dist/src/ContentOverlay/UNSAFE_WrappedModalize.js +0 -23
  36. package/dist/types/src/ContentOverlay/UNSAFE_WrappedModalize.d.ts +0 -3
  37. package/src/ContentOverlay/UNSAFE_WrappedModalize.tsx +0 -41
@@ -1,228 +1,204 @@
1
- import type { Ref } from "react";
2
- import React, {
3
- forwardRef,
4
- useCallback,
5
- useImperativeHandle,
6
- useMemo,
7
- useRef,
8
- useState,
9
- } from "react";
10
- import type { Modalize } from "react-native-modalize";
11
- import { useSafeAreaInsets } from "react-native-safe-area-context";
12
- import type { NativeScrollEvent, NativeSyntheticEvent } from "react-native";
1
+ import React, { useImperativeHandle, useMemo, useRef, useState } from "react";
13
2
  import {
14
3
  AccessibilityInfo,
15
- Platform,
16
4
  View,
17
5
  findNodeHandle,
18
6
  useWindowDimensions,
19
7
  } from "react-native";
20
- import { Portal } from "react-native-portalize";
21
- import { useKeyboardVisibility } from "./hooks/useKeyboardVisibility";
22
- import { useStyles } from "./ContentOverlay.style";
23
- import { useViewLayoutHeight } from "./hooks/useViewLayoutHeight";
8
+ import { useSafeAreaInsets } from "react-native-safe-area-context";
9
+ import {
10
+ BottomSheetBackdrop,
11
+ BottomSheetModal,
12
+ BottomSheetScrollView,
13
+ BottomSheetView,
14
+ } from "@gorhom/bottom-sheet";
24
15
  import type {
25
- ContentOverlayProps,
26
- ContentOverlayRef,
27
- ModalBackgroundColor,
28
- } from "./types";
29
- import { UNSAFE_WrappedModalize } from "./UNSAFE_WrappedModalize";
16
+ BottomSheetBackdropProps,
17
+ BottomSheetModal as BottomSheetModalType,
18
+ BottomSheetScrollViewMethods,
19
+ } from "@gorhom/bottom-sheet";
20
+ import type { ContentOverlayProps, ModalBackgroundColor } from "./types";
21
+ import { useStyles } from "./ContentOverlay.style";
22
+ import { useBottomSheetModalBackHandler } from "./hooks/useBottomSheetModalBackHandler";
23
+ import { computeContentOverlayBehavior } from "./computeContentOverlayBehavior";
30
24
  import { useIsScreenReaderEnabled } from "../hooks";
31
25
  import { IconButton } from "../IconButton";
32
26
  import { Heading } from "../Heading";
33
27
  import { useAtlantisI18n } from "../hooks/useAtlantisI18n";
34
28
  import { useAtlantisTheme } from "../AtlantisThemeContext";
35
29
 
36
- export const ContentOverlay = forwardRef(ContentOverlayPortal);
37
- const ContentOverlayModal = forwardRef(ContentOverlayInternal);
30
+ const LARGE_SCREEN_BREAKPOINT = 640;
38
31
 
39
- // eslint-disable-next-line max-statements
40
- function ContentOverlayInternal(
41
- {
42
- children,
43
- title,
44
- accessibilityLabel,
45
- fullScreen = false,
46
- showDismiss = false,
47
- isDraggable = true,
48
- adjustToContentHeight = false,
49
- keyboardShouldPersistTaps = false,
50
- keyboardAvoidingBehavior,
51
- scrollEnabled = false,
52
- modalBackgroundColor = "surface",
53
- onClose,
54
- onOpen,
55
- onBeforeExit,
56
- loading = false,
57
- avoidKeyboardLikeIOS,
58
- }: ContentOverlayProps,
59
- ref: Ref<ContentOverlayRef>,
32
+ function getModalBackgroundColor(
33
+ variation: ModalBackgroundColor,
34
+ tokens: ReturnType<typeof useAtlantisTheme>["tokens"],
60
35
  ) {
61
- isDraggable = onBeforeExit ? false : isDraggable;
62
- const isCloseableOnOverlayTap = onBeforeExit ? false : true;
36
+ switch (variation) {
37
+ case "surface":
38
+ return tokens["color-surface"];
39
+ case "background":
40
+ return tokens["color-surface--background"];
41
+ }
42
+ }
43
+
44
+ // eslint-disable-next-line max-statements
45
+ export function ContentOverlay({
46
+ children,
47
+ title,
48
+ accessibilityLabel,
49
+ fullScreen = false,
50
+ showDismiss = false,
51
+ isDraggable = true,
52
+ adjustToContentHeight = false,
53
+ keyboardShouldPersistTaps = false,
54
+ scrollEnabled = false,
55
+ modalBackgroundColor = "surface",
56
+ onClose,
57
+ onOpen,
58
+ onBeforeExit,
59
+ loading = false,
60
+ ref,
61
+ }: ContentOverlayProps) {
62
+ const insets = useSafeAreaInsets();
63
+ const { width: windowWidth } = useWindowDimensions();
64
+ const bottomSheetModalRef = useRef<BottomSheetModalType>(null);
65
+ const previousIndexRef = useRef(-1);
66
+ const [currentPosition, setCurrentPosition] = useState<number>(-1);
67
+
68
+ const styles = useStyles();
63
69
  const { t } = useAtlantisI18n();
64
70
  const { tokens } = useAtlantisTheme();
65
- const { width: windowWidth, height: windowHeight } = useWindowDimensions();
66
- const insets = useSafeAreaInsets();
67
- const [position, setPosition] = useState<"top" | "initial">("initial");
68
71
  const isScreenReaderEnabled = useIsScreenReaderEnabled();
69
- const isFullScreenOrTopPosition =
70
- fullScreen || (!adjustToContentHeight && position === "top");
71
- const shouldShowDismiss =
72
- showDismiss || isScreenReaderEnabled || isFullScreenOrTopPosition;
73
- const [showHeaderShadow, setShowHeaderShadow] = useState<boolean>(false);
74
- const overlayHeader = useRef<View>(null);
75
-
76
- const internalRef = useRef<Modalize>(null);
77
- const [modalizeMethods, setModalizeMethods] = useState<ContentOverlayRef>();
78
- const callbackInternalRef = useCallback((instance: Modalize) => {
79
- if (instance && !internalRef.current) {
80
- internalRef.current = instance;
81
- setModalizeMethods(instance);
82
- }
83
- }, []);
84
72
 
85
- const refMethods = useMemo(() => {
86
- if (!modalizeMethods?.open || !modalizeMethods?.close) {
87
- return {};
88
- }
73
+ const behavior = computeContentOverlayBehavior(
74
+ {
75
+ fullScreen,
76
+ adjustToContentHeight,
77
+ isDraggable,
78
+ hasOnBeforeExit: onBeforeExit !== undefined,
79
+ showDismiss,
80
+ },
81
+ {
82
+ isScreenReaderEnabled,
83
+ position: currentPosition,
84
+ },
85
+ );
89
86
 
90
- return {
91
- open: modalizeMethods?.open,
92
- close: modalizeMethods?.close,
93
- };
94
- }, [modalizeMethods]);
87
+ const effectiveIsDraggable = behavior.isDraggable;
88
+ const shouldShowDismiss = behavior.showDismiss;
89
+ const isCloseableOnOverlayTap = onBeforeExit === undefined;
95
90
 
96
- const { keyboardHeight } = useKeyboardVisibility();
97
- useImperativeHandle(ref, () => refMethods, [refMethods]);
91
+ // Prevent the Overlay from being flush with the top of the screen, even if we are "100%" or "fullscreen"
92
+ const topInset = insets.top || tokens["space-larger"];
98
93
 
99
- const {
100
- handleLayout: handleChildrenLayout,
101
- height: childrenHeight,
102
- heightKnown: childrenHeightKnown,
103
- } = useViewLayoutHeight();
104
- const {
105
- handleLayout: handleHeaderLayout,
106
- height: headerHeight,
107
- heightKnown: headerHeightKnown,
108
- } = useViewLayoutHeight();
94
+ const [showHeaderShadow, setShowHeaderShadow] = useState<boolean>(false);
95
+ const overlayHeader = useRef<View>(null);
96
+ const scrollViewRef = useRef<
97
+ BottomSheetScrollViewMethods & { scrollTop?: number }
98
+ >(null);
109
99
 
110
- const snapPoint = useMemo(() => {
111
- if (fullScreen || !isDraggable || adjustToContentHeight) {
112
- return undefined;
113
- }
114
- const overlayHeight = headerHeight + childrenHeight;
100
+ // enableDynamicSizing will add another snap point of the content height
101
+ const snapPoints = useMemo(() => {
102
+ // There is a bug with "restore" behavior after keyboard is dismissed.
103
+ // https://github.com/gorhom/react-native-bottom-sheet/issues/2465
104
+ // providing a 100% snap point "fixes" it for now, but there is an approved PR to fix it
105
+ // that just needs to be merged and released: https://github.com/gorhom/react-native-bottom-sheet/pull/2511
106
+ return ["100%"];
107
+ }, []);
115
108
 
116
- if (overlayHeight >= windowHeight) {
117
- return undefined;
109
+ const onCloseController = () => {
110
+ if (!onBeforeExit) {
111
+ bottomSheetModalRef.current?.dismiss();
112
+ } else {
113
+ onBeforeExit();
118
114
  }
115
+ };
119
116
 
120
- return overlayHeight;
121
- }, [
122
- fullScreen,
123
- isDraggable,
124
- adjustToContentHeight,
125
- headerHeight,
126
- childrenHeight,
127
- windowHeight,
128
- ]);
117
+ const { handleSheetPositionChange } =
118
+ useBottomSheetModalBackHandler(onCloseController);
129
119
 
130
- const styles = useStyles();
120
+ useImperativeHandle(ref, () => ({
121
+ open: () => {
122
+ bottomSheetModalRef.current?.present();
123
+ },
124
+ close: () => {
125
+ bottomSheetModalRef.current?.dismiss();
126
+ },
127
+ }));
131
128
 
132
- const modalStyle = [
133
- styles.modal,
134
- windowWidth > 640 ? styles.modalForLargeScreens : undefined,
135
- { backgroundColor: getModalBackgroundColor(modalBackgroundColor) },
136
- keyboardHeight > 0 && { marginBottom: 0 },
137
- ];
129
+ const handleChange = (index: number, position: number) => {
130
+ const previousIndex = previousIndexRef.current;
138
131
 
139
- const renderedChildren = renderChildren();
140
- const renderedHeader = renderHeader();
132
+ setCurrentPosition(position);
133
+ handleSheetPositionChange(index);
141
134
 
142
- const onCloseController = () => {
143
- if (!onBeforeExit) {
144
- internalRef.current?.close();
135
+ if (previousIndex === -1 && index >= 0) {
136
+ // Transitioned from closed to open
137
+ onOpen?.();
145
138
 
146
- return true;
147
- } else {
148
- onBeforeExit();
139
+ // Set accessibility focus on header when opened
140
+ if (overlayHeader.current) {
141
+ const reactTag = findNodeHandle(overlayHeader.current);
149
142
 
150
- return false;
143
+ if (reactTag) {
144
+ AccessibilityInfo.setAccessibilityFocus(reactTag);
145
+ }
146
+ }
151
147
  }
148
+
149
+ previousIndexRef.current = index;
152
150
  };
153
151
 
154
- return (
155
- <>
156
- {headerHeightKnown && childrenHeightKnown && (
157
- <UNSAFE_WrappedModalize
158
- ref={callbackInternalRef}
159
- overlayStyle={styles.overlay}
160
- handleStyle={styles.handle}
161
- handlePosition="inside"
162
- modalStyle={modalStyle}
163
- modalTopOffset={tokens["space-larger"]}
164
- snapPoint={snapPoint}
165
- closeSnapPointStraightEnabled={false}
166
- withHandle={isDraggable}
167
- panGestureEnabled={isDraggable}
168
- adjustToContentHeight={adjustToContentHeight}
169
- disableScrollIfPossible={!adjustToContentHeight} // workaround for scroll not working on Android when content fills the screen with adjustToContentHeight
170
- onClose={onClose}
171
- onOpen={onOpen}
172
- keyboardAvoidingBehavior={keyboardAvoidingBehavior}
173
- avoidKeyboardLikeIOS={avoidKeyboardLikeIOS}
174
- childrenStyle={styles.childrenStyle}
175
- onBackButtonPress={onCloseController}
176
- closeOnOverlayTap={isCloseableOnOverlayTap}
177
- onOpened={() => {
178
- if (overlayHeader.current) {
179
- const reactTag = findNodeHandle(overlayHeader.current);
152
+ const handleOnScroll = () => {
153
+ const scrollTop = scrollViewRef.current?.scrollTop || 0;
154
+ setShowHeaderShadow(scrollTop > 0);
155
+ };
180
156
 
181
- if (reactTag) {
182
- AccessibilityInfo.setAccessibilityFocus(reactTag);
183
- }
184
- }
185
- }}
186
- scrollViewProps={{
187
- scrollEnabled,
188
- showsVerticalScrollIndicator: false,
189
- stickyHeaderIndices: Platform.OS === "android" ? [0] : undefined,
190
- onScroll: handleOnScroll,
191
- keyboardShouldPersistTaps: keyboardShouldPersistTaps
192
- ? "handled"
193
- : "never",
194
- }}
195
- HeaderComponent={Platform.OS === "ios" ? renderedHeader : undefined}
196
- onPositionChange={setPosition}
197
- >
198
- {Platform.OS === "android" ? renderedHeader : undefined}
199
- {renderedChildren}
200
- </UNSAFE_WrappedModalize>
201
- )}
202
- {!childrenHeightKnown && (
203
- <View style={[styles.hiddenContent, modalStyle]}>
204
- {renderedChildren}
205
- </View>
206
- )}
207
- {!headerHeightKnown && (
208
- <View style={[styles.hiddenContent, modalStyle]}>{renderedHeader}</View>
209
- )}
210
- </>
157
+ const sheetStyle = useMemo(
158
+ () =>
159
+ windowWidth > LARGE_SCREEN_BREAKPOINT
160
+ ? {
161
+ width: LARGE_SCREEN_BREAKPOINT,
162
+ marginLeft: (windowWidth - LARGE_SCREEN_BREAKPOINT) / 2,
163
+ }
164
+ : undefined,
165
+ [windowWidth],
211
166
  );
212
167
 
213
- function renderHeader() {
168
+ const backgroundStyle = [
169
+ styles.background,
170
+ { backgroundColor: getModalBackgroundColor(modalBackgroundColor, tokens) },
171
+ ];
172
+
173
+ const handleIndicatorStyles = [
174
+ styles.handle,
175
+ !effectiveIsDraggable && {
176
+ opacity: 0,
177
+ },
178
+ ];
179
+
180
+ const renderHeader = () => {
214
181
  const closeOverlayA11YLabel = t("ContentOverlay.close", {
215
182
  title: title,
216
183
  });
217
184
 
218
185
  const headerStyles = [
219
186
  styles.header,
187
+ {
188
+ // Background color is necessary for scrollable modals as the content flows behind the header.
189
+ backgroundColor: getModalBackgroundColor(modalBackgroundColor, tokens),
190
+ },
191
+ ];
192
+
193
+ const headerShadowStyles = [
220
194
  showHeaderShadow && styles.headerShadow,
221
- { backgroundColor: getModalBackgroundColor(modalBackgroundColor) },
195
+ {
196
+ backgroundColor: getModalBackgroundColor(modalBackgroundColor, tokens),
197
+ },
222
198
  ];
223
199
 
224
200
  return (
225
- <View onLayout={handleHeaderLayout} testID="ATL-Overlay-Header">
201
+ <View testID="ATL-Overlay-Header">
226
202
  <View style={headerStyles}>
227
203
  <View
228
204
  style={[
@@ -260,45 +236,82 @@ function ContentOverlayInternal(
260
236
  </View>
261
237
  )}
262
238
  </View>
239
+ <View>
240
+ <View style={headerShadowStyles} />
241
+ </View>
263
242
  </View>
264
243
  );
265
- }
266
-
267
- function renderChildren() {
268
- return (
269
- <View
270
- style={{ paddingBottom: insets.bottom }}
271
- onLayout={handleChildrenLayout}
272
- testID="ATL-Overlay-Children"
273
- >
274
- {children}
275
- </View>
276
- );
277
- }
278
-
279
- function handleOnScroll({
280
- nativeEvent,
281
- }: NativeSyntheticEvent<NativeScrollEvent>) {
282
- setShowHeaderShadow(nativeEvent.contentOffset.y > 0);
283
- }
244
+ };
284
245
 
285
- function getModalBackgroundColor(variation: ModalBackgroundColor) {
286
- switch (variation) {
287
- case "surface":
288
- return tokens["color-surface"];
289
- case "background":
290
- return tokens["color-surface--background"];
291
- }
292
- }
246
+ return (
247
+ <BottomSheetModal
248
+ ref={bottomSheetModalRef}
249
+ onChange={handleChange}
250
+ style={sheetStyle}
251
+ backgroundStyle={backgroundStyle}
252
+ handleStyle={styles.handleWrapper}
253
+ handleIndicatorStyle={handleIndicatorStyles}
254
+ backdropComponent={props => (
255
+ <Backdrop
256
+ {...props}
257
+ pressBehavior={isCloseableOnOverlayTap ? "close" : "none"}
258
+ />
259
+ )}
260
+ snapPoints={snapPoints}
261
+ enablePanDownToClose={effectiveIsDraggable}
262
+ enableContentPanningGesture={effectiveIsDraggable}
263
+ enableHandlePanningGesture={effectiveIsDraggable}
264
+ enableDynamicSizing={behavior.initialHeight === "contentHeight"}
265
+ keyboardBehavior="interactive"
266
+ keyboardBlurBehavior="restore"
267
+ topInset={topInset}
268
+ onDismiss={() => onClose?.()}
269
+ >
270
+ {scrollEnabled ? (
271
+ <BottomSheetScrollView
272
+ ref={scrollViewRef}
273
+ contentContainerStyle={{ paddingBottom: insets.bottom }}
274
+ keyboardShouldPersistTaps={
275
+ keyboardShouldPersistTaps ? "handled" : "never"
276
+ }
277
+ showsVerticalScrollIndicator={false}
278
+ onScroll={handleOnScroll}
279
+ stickyHeaderIndices={[0]}
280
+ >
281
+ {renderHeader()}
282
+ <View testID="ATL-Overlay-Children">{children}</View>
283
+ </BottomSheetScrollView>
284
+ ) : (
285
+ <BottomSheetView>
286
+ {renderHeader()}
287
+ <View
288
+ style={{ paddingBottom: insets.bottom }}
289
+ testID="ATL-Overlay-Children"
290
+ >
291
+ {children}
292
+ </View>
293
+ </BottomSheetView>
294
+ )}
295
+ </BottomSheetModal>
296
+ );
293
297
  }
294
298
 
295
- function ContentOverlayPortal(
296
- modalProps: ContentOverlayProps,
297
- ref: Ref<ContentOverlayRef>,
299
+ function Backdrop(
300
+ bottomSheetBackdropProps: BottomSheetBackdropProps & {
301
+ pressBehavior: "none" | "close";
302
+ },
298
303
  ) {
304
+ const styles = useStyles();
305
+ const { pressBehavior, ...props } = bottomSheetBackdropProps;
306
+
299
307
  return (
300
- <Portal>
301
- <ContentOverlayModal ref={ref} {...modalProps} />
302
- </Portal>
308
+ <BottomSheetBackdrop
309
+ {...props}
310
+ appearsOnIndex={0}
311
+ disappearsOnIndex={-1}
312
+ style={styles.backdrop}
313
+ opacity={1}
314
+ pressBehavior={pressBehavior}
315
+ />
303
316
  );
304
317
  }
@@ -0,0 +1,12 @@
1
+ import React from "react";
2
+ import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
3
+
4
+ interface ContentOverlayProviderProps {
5
+ readonly children: React.ReactNode;
6
+ }
7
+
8
+ export function ContentOverlayProvider({
9
+ children,
10
+ }: ContentOverlayProviderProps) {
11
+ return <BottomSheetModalProvider>{children}</BottomSheetModalProvider>;
12
+ }