@jobber/components-native 0.105.4 → 0.105.5-contentove-4cd673f.10

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.
@@ -1,4 +1,4 @@
1
1
  import React from "react";
2
2
  import type { ContentOverlayProps } from "./types";
3
3
  export declare function useIsKeyboardHandledByScrollView(): boolean;
4
- export declare function ContentOverlay({ children, title, accessibilityLabel, fullScreen, showDismiss, isDraggable, adjustToContentHeight, keyboardShouldPersistTaps, scrollEnabled, modalBackgroundColor, onClose, onOpen, onBeforeExit, loading, ref, }: ContentOverlayProps): React.JSX.Element;
4
+ export declare function ContentOverlay({ children, title, accessibilityLabel, fullScreen, showDismiss, isDraggable, adjustToContentHeight, keyboardShouldPersistTaps, scrollEnabled, modalBackgroundColor, footerComponent, onClose, onOpen, onBeforeExit, loading, ref, }: ContentOverlayProps): React.JSX.Element;
@@ -46,6 +46,18 @@ export interface ContentOverlayProps {
46
46
  * Enables scrolling in the content body of overlay
47
47
  */
48
48
  readonly scrollEnabled?: boolean;
49
+ /**
50
+ * Content pinned to the bottom of the overlay, above the keyboard — e.g. a
51
+ * persistent search input (results scroll above it) or persistent action
52
+ * buttons. Rendered outside the scrollable body via the bottom sheet's
53
+ * footer, so it stays visible while the body scrolls and rises with the
54
+ * keyboard. When set, the overlay uses the bottom sheet's own keyboard
55
+ * handling rather than the keyboard-aware scroll view, and sizes to its
56
+ * content. Intended for use with content-height overlays (the default, or
57
+ * `adjustToContentHeight`).
58
+ * @default undefined
59
+ */
60
+ readonly footerComponent?: ReactNode;
49
61
  /**
50
62
  * Set the background color of the modal window
51
63
  * @default "surface"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jobber/components-native",
3
- "version": "0.105.4",
3
+ "version": "0.105.5-contentove-4cd673f.10+4cd673ff",
4
4
  "license": "MIT",
5
5
  "description": "React Native implementation of Atlantis",
6
6
  "repository": {
@@ -122,5 +122,5 @@
122
122
  "react-native-screens": ">=4.18.0",
123
123
  "react-native-svg": ">=12.0.0"
124
124
  },
125
- "gitHead": "7fb12fadcd1c5e1440bccccae13dd29cf74dae28"
125
+ "gitHead": "4cd673ff20841cf9e6ea375cf0577310ffaae365"
126
126
  }
@@ -2,7 +2,7 @@ import type { ComponentProps } from "react";
2
2
  import type { Meta, StoryObj } from "@storybook/react-native-web-vite";
3
3
  import { action } from "storybook/actions";
4
4
  import { ContentOverlay } from "@jobber/components-native";
5
- import { ContentOverlayBasic } from "./docs";
5
+ import { ContentOverlayBasic, ContentOverlayWithFooter } from "./docs";
6
6
 
7
7
  const meta = {
8
8
  title: "Components/Overlays/ContentOverlay",
@@ -28,3 +28,17 @@ export const Basic: Story = {
28
28
  fullScreen: false,
29
29
  },
30
30
  };
31
+
32
+ /**
33
+ * Persistent-input + scrolling-results pattern (team mentions picker): the
34
+ * result list scrolls in the body while the search input stays pinned at the
35
+ * bottom via `footerComponent`, above the keyboard. The overlay sizes to its
36
+ * content rather than filling the screen.
37
+ */
38
+ export const WithFooter: Story = {
39
+ render: ContentOverlayWithFooter,
40
+ args: {
41
+ onClose: () => action("alert")("Overlay Dismissed"),
42
+ onOpen: () => action("alert")("Overlay opened"),
43
+ },
44
+ };
@@ -480,3 +480,59 @@ describe("title prop", () => {
480
480
  });
481
481
  });
482
482
  });
483
+
484
+ describe("when a footerComponent is provided", () => {
485
+ function renderWithFooter() {
486
+ const contentOverlayRef = createRef<ContentOverlayRef>();
487
+
488
+ render(
489
+ <AtlantisOverlayProvider>
490
+ <View>
491
+ <Button
492
+ label="Open Content Overlay"
493
+ onPress={() => contentOverlayRef.current?.open?.()}
494
+ />
495
+ <ContentOverlay
496
+ ref={contentOverlayRef}
497
+ adjustToContentHeight
498
+ scrollEnabled
499
+ onOpen={jest.fn()}
500
+ footerComponent={<Text>Search team</Text>}
501
+ >
502
+ <Content>
503
+ <Text>Marty Fayes</Text>
504
+ </Content>
505
+ </ContentOverlay>
506
+ </View>
507
+ </AtlantisOverlayProvider>,
508
+ );
509
+
510
+ return contentOverlayRef;
511
+ }
512
+
513
+ async function openWithFooter() {
514
+ jest.useFakeTimers();
515
+ renderWithFooter();
516
+ await user.press(screen.getByLabelText("Open Content Overlay"));
517
+ await act(async () => {
518
+ jest.runAllTimers();
519
+ });
520
+ }
521
+
522
+ it("renders the footer content alongside the body content", async () => {
523
+ await openWithFooter();
524
+
525
+ await waitFor(() => {
526
+ expect(screen.getByText("Search team")).toBeDefined();
527
+ expect(screen.getByText("Marty Fayes")).toBeDefined();
528
+ });
529
+ });
530
+
531
+ it("still exposes the overlay body content container", async () => {
532
+ await openWithFooter();
533
+
534
+ await waitFor(() => {
535
+ expect(screen.getByTestId("ATL-Overlay-Children")).toBeDefined();
536
+ });
537
+ });
538
+ });
@@ -18,6 +18,7 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";
18
18
  import {
19
19
  BottomSheetBackdrop,
20
20
  BottomSheetModal,
21
+ BottomSheetScrollView,
21
22
  BottomSheetView,
22
23
  } from "@gorhom/bottom-sheet";
23
24
  import type {
@@ -30,6 +31,7 @@ import { BottomSheetKeyboardAwareScrollView } from "./BottomSheetKeyboardAwareSc
30
31
  import type { ContentOverlayProps, ModalBackgroundColor } from "./types";
31
32
  import { useStyles } from "./ContentOverlay.style";
32
33
  import { useBottomSheetModalBackHandler } from "./hooks/useBottomSheetModalBackHandler";
34
+ import { useKeyboardVisibility } from "./hooks/useKeyboardVisibility";
33
35
  import { computeContentOverlayBehavior } from "./computeContentOverlayBehavior";
34
36
  import { KEYBOARD_TOP_PADDING_AUTO_SCROLL } from "./constants";
35
37
  import { useIsScreenReaderEnabled } from "../hooks";
@@ -78,6 +80,7 @@ export function ContentOverlay({
78
80
  keyboardShouldPersistTaps = false,
79
81
  scrollEnabled = false,
80
82
  modalBackgroundColor = "surface",
83
+ footerComponent,
81
84
  onClose,
82
85
  onOpen,
83
86
  onBeforeExit,
@@ -85,6 +88,7 @@ export function ContentOverlay({
85
88
  ref,
86
89
  }: ContentOverlayProps) {
87
90
  const insets = useSafeAreaInsets();
91
+ const { isKeyboardVisible } = useKeyboardVisibility();
88
92
  const { width: windowWidth } = useWindowDimensions();
89
93
  const bottomSheetModalRef = useRef<BottomSheetModalType>(null);
90
94
  const previousIndexRef = useRef(-1);
@@ -111,6 +115,8 @@ export function ContentOverlay({
111
115
 
112
116
  const effectiveIsDraggable = behavior.isDraggable;
113
117
  const shouldShowDismiss = behavior.showDismiss;
118
+ const hasHeader = Boolean(title || shouldShowDismiss);
119
+ const stickyHeaderIndices = hasHeader ? [0] : undefined;
114
120
  const isCloseableOnOverlayTap = onBeforeExit === undefined;
115
121
 
116
122
  // Prevent the Overlay from being flush with the top of the screen, even if we are "100%" or "fullscreen"
@@ -122,12 +128,15 @@ export function ContentOverlay({
122
128
  BottomSheetScrollViewMethods & { scrollTop?: number }
123
129
  >(null);
124
130
 
131
+ const hasFooter = footerComponent !== undefined;
132
+
125
133
  // enableDynamicSizing will add another snap point of the content height
126
134
  const snapPoints = useMemo(() => {
127
- // There is a bug with "restore" behavior after keyboard is dismissed.
135
+ // There is a bug with "restore" behavior after the keyboard is dismissed.
128
136
  // https://github.com/gorhom/react-native-bottom-sheet/issues/2465
129
- // providing a 100% snap point "fixes" it for now, but there is an approved PR to fix it
130
- // that just needs to be merged and released: https://github.com/gorhom/react-native-bottom-sheet/pull/2511
137
+ // Providing a 100% snap point "fixes" it for now. The upstream fix
138
+ // (https://github.com/gorhom/react-native-bottom-sheet/pull/2511) was
139
+ // closed without merging, so there is no released fix to wait for.
131
140
  return ["100%"];
132
141
  }, []);
133
142
 
@@ -297,7 +306,7 @@ export function ContentOverlay({
297
306
  backdropComponent={backdropComponent}
298
307
  snapPoints={snapPoints}
299
308
  enablePanDownToClose={effectiveIsDraggable}
300
- enableContentPanningGesture={effectiveIsDraggable}
309
+ enableContentPanningGesture={effectiveIsDraggable || scrollEnabled}
301
310
  enableHandlePanningGesture={effectiveIsDraggable}
302
311
  enableDynamicSizing={behavior.initialHeight === "contentHeight"}
303
312
  keyboardBehavior="interactive"
@@ -305,8 +314,40 @@ export function ContentOverlay({
305
314
  topInset={topInset}
306
315
  onDismiss={() => onClose?.()}
307
316
  >
308
- <ContentOverlayKeyboardContext.Provider value={scrollEnabled}>
309
- {scrollEnabled ? (
317
+ <ContentOverlayKeyboardContext.Provider
318
+ value={scrollEnabled && !hasFooter}
319
+ >
320
+ {hasFooter ? (
321
+ <View style={{ flex: 1 }}>
322
+ {scrollEnabled ? (
323
+ <BottomSheetScrollView
324
+ ref={scrollViewRef}
325
+ style={{ flex: 1 }}
326
+ keyboardShouldPersistTaps={
327
+ keyboardShouldPersistTaps ? "handled" : "never"
328
+ }
329
+ showsVerticalScrollIndicator={false}
330
+ onScroll={handleOnScroll}
331
+ stickyHeaderIndices={stickyHeaderIndices}
332
+ >
333
+ {renderHeader()}
334
+ <View testID="ATL-Overlay-Children">{children}</View>
335
+ </BottomSheetScrollView>
336
+ ) : (
337
+ <BottomSheetView style={{ flex: 1 }}>
338
+ {renderHeader()}
339
+ <View testID="ATL-Overlay-Children">{children}</View>
340
+ </BottomSheetView>
341
+ )}
342
+ <View
343
+ style={{
344
+ paddingBottom: isKeyboardVisible ? 0 : insets.bottom,
345
+ }}
346
+ >
347
+ {footerComponent}
348
+ </View>
349
+ </View>
350
+ ) : scrollEnabled ? (
310
351
  <BottomSheetKeyboardAwareScrollView
311
352
  ref={scrollViewRef}
312
353
  contentContainerStyle={{ paddingBottom: insets.bottom }}
@@ -315,7 +356,7 @@ export function ContentOverlay({
315
356
  }
316
357
  showsVerticalScrollIndicator={false}
317
358
  onScroll={handleOnScroll}
318
- stickyHeaderIndices={[0]}
359
+ stickyHeaderIndices={stickyHeaderIndices}
319
360
  bottomOffset={KEYBOARD_TOP_PADDING_AUTO_SCROLL}
320
361
  >
321
362
  {renderHeader()}
@@ -0,0 +1,85 @@
1
+ import React, { useRef, useState } from "react";
2
+ import type { ComponentProps } from "react";
3
+ import { Pressable, View } from "react-native";
4
+ import { SafeAreaProvider } from "react-native-safe-area-context";
5
+ import { action } from "storybook/actions";
6
+ import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
7
+ import type { ContentOverlayRef } from "@jobber/components-native";
8
+ import {
9
+ Button,
10
+ ContentOverlay,
11
+ Heading,
12
+ InputText,
13
+ Text,
14
+ } from "@jobber/components-native";
15
+
16
+ type ContentOverlayProps = ComponentProps<typeof ContentOverlay>;
17
+
18
+ const TEAM = [
19
+ "Marty Fayes",
20
+ "Damien Henriksson",
21
+ "Olivia Hughes",
22
+ "Priya Anand",
23
+ "Diego Marquez",
24
+ "Sam Okonkwo",
25
+ "Yuki Tanaka",
26
+ "Noor Haddad",
27
+ ];
28
+
29
+ /**
30
+ * Demonstrates the persistent-input + scrolling-results pattern (the team
31
+ * mentions picker): the result list scrolls in the body while the search input
32
+ * stays pinned to the bottom via `footerComponent`, above the keyboard.
33
+ */
34
+ export function ContentOverlayWithFooter(props: Partial<ContentOverlayProps>) {
35
+ const contentOverlayRef = useRef<ContentOverlayRef>(null);
36
+ const [query, setQuery] = useState("");
37
+
38
+ const matches = TEAM.filter(name =>
39
+ name.toLowerCase().includes(query.toLowerCase()),
40
+ );
41
+
42
+ return (
43
+ <SafeAreaProvider>
44
+ <BottomSheetModalProvider>
45
+ <View style={{ display: "flex", flexDirection: "column", gap: 16 }}>
46
+ <Heading>Mentions picker (footer input)</Heading>
47
+ <Button
48
+ label="Open mentions picker"
49
+ onPress={() => contentOverlayRef.current?.open?.()}
50
+ />
51
+ </View>
52
+ <ContentOverlay
53
+ ref={contentOverlayRef}
54
+ adjustToContentHeight
55
+ scrollEnabled
56
+ onClose={() => action("console.log")("closed")}
57
+ onOpen={() => action("console.log")("opened")}
58
+ footerComponent={
59
+ <View style={{ padding: 16 }}>
60
+ <InputText
61
+ placeholder="Search team"
62
+ value={query}
63
+ onChangeText={setQuery}
64
+ clearable="always"
65
+ />
66
+ </View>
67
+ }
68
+ {...props}
69
+ >
70
+ <View style={{ paddingHorizontal: 16, gap: 8 }}>
71
+ {matches.map(name => (
72
+ <Pressable
73
+ key={name}
74
+ onPress={() => contentOverlayRef.current?.close?.()}
75
+ style={{ paddingVertical: 12 }}
76
+ >
77
+ <Text>{name}</Text>
78
+ </Pressable>
79
+ ))}
80
+ </View>
81
+ </ContentOverlay>
82
+ </BottomSheetModalProvider>
83
+ </SafeAreaProvider>
84
+ );
85
+ }
@@ -1 +1,2 @@
1
1
  export { ContentOverlayBasic } from "./ContentOverlayBasic";
2
+ export { ContentOverlayWithFooter } from "./ContentOverlayWithFooter";
@@ -47,6 +47,18 @@ export interface ContentOverlayProps {
47
47
  * Enables scrolling in the content body of overlay
48
48
  */
49
49
  readonly scrollEnabled?: boolean;
50
+ /**
51
+ * Content pinned to the bottom of the overlay, above the keyboard — e.g. a
52
+ * persistent search input (results scroll above it) or persistent action
53
+ * buttons. Rendered outside the scrollable body via the bottom sheet's
54
+ * footer, so it stays visible while the body scrolls and rises with the
55
+ * keyboard. When set, the overlay uses the bottom sheet's own keyboard
56
+ * handling rather than the keyboard-aware scroll view, and sizes to its
57
+ * content. Intended for use with content-height overlays (the default, or
58
+ * `adjustToContentHeight`).
59
+ * @default undefined
60
+ */
61
+ readonly footerComponent?: ReactNode;
50
62
  /**
51
63
  * Set the background color of the modal window
52
64
  * @default "surface"