@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.
- package/dist/docs/Chip/Chip.md +4 -4
- package/dist/docs/ContentOverlay/ContentOverlay.md +1 -0
- package/dist/docs/usage-guidelines/usage-guidelines.md +5 -5
- package/dist/package.json +2 -2
- package/dist/src/ContentOverlay/ContentOverlay.js +21 -7
- package/dist/src/ContentOverlay/ContentOverlay.test.js +35 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/dist/types/src/ContentOverlay/ContentOverlay.d.ts +1 -1
- package/dist/types/src/ContentOverlay/types.d.ts +12 -0
- package/package.json +2 -2
- package/src/ContentOverlay/ContentOverlay.stories.tsx +15 -1
- package/src/ContentOverlay/ContentOverlay.test.tsx +56 -0
- package/src/ContentOverlay/ContentOverlay.tsx +48 -7
- package/src/ContentOverlay/docs/ContentOverlayWithFooter.tsx +85 -0
- package/src/ContentOverlay/docs/index.ts +1 -0
- package/src/ContentOverlay/types.ts +12 -0
|
@@ -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.
|
|
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": "
|
|
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
|
-
//
|
|
130
|
-
//
|
|
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
|
|
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={
|
|
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
|
+
}
|
|
@@ -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"
|