@jobber/components-native 0.24.1-migrate-te.4 → 0.25.1-fix-breakp.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/src/BottomSheet/BottomSheet.js +52 -0
- package/dist/src/BottomSheet/BottomSheet.style.js +28 -0
- package/dist/src/BottomSheet/components/BottomSheetOption/BottomSheetOption.js +17 -0
- package/dist/src/BottomSheet/components/BottomSheetOption/BottomSheetOption.styles.js +18 -0
- package/dist/src/BottomSheet/components/BottomSheetOption/index.js +1 -0
- package/dist/src/BottomSheet/index.js +1 -0
- package/dist/src/BottomSheet/messages.js +8 -0
- package/dist/src/hooks/index.js +1 -0
- package/dist/src/hooks/useIsScreenReaderEnabled.js +22 -0
- package/dist/src/index.js +1 -1
- package/dist/src/utils/test/wait.js +58 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/src/BottomSheet/BottomSheet.d.ts +28 -0
- package/dist/types/src/BottomSheet/BottomSheet.style.d.ts +32 -0
- package/dist/types/src/BottomSheet/components/BottomSheetOption/BottomSheetOption.d.ts +13 -0
- package/dist/types/src/BottomSheet/components/BottomSheetOption/BottomSheetOption.styles.d.ts +16 -0
- package/dist/types/src/BottomSheet/components/BottomSheetOption/index.d.ts +1 -0
- package/dist/types/src/BottomSheet/index.d.ts +1 -0
- package/dist/types/src/BottomSheet/messages.d.ts +7 -0
- package/dist/types/src/hooks/index.d.ts +1 -0
- package/dist/types/src/hooks/useIsScreenReaderEnabled.d.ts +1 -0
- package/dist/types/src/index.d.ts +1 -1
- package/dist/types/src/utils/test/wait.d.ts +36 -0
- package/package.json +7 -5
- package/src/BottomSheet/BottomSheet.style.ts +35 -0
- package/src/BottomSheet/BottomSheet.test.tsx +152 -0
- package/src/BottomSheet/BottomSheet.tsx +149 -0
- package/src/BottomSheet/components/BottomSheetOption/BottomSheetOption.styles.ts +19 -0
- package/src/BottomSheet/components/BottomSheetOption/BottomSheetOption.test.tsx +34 -0
- package/src/BottomSheet/components/BottomSheetOption/BottomSheetOption.tsx +53 -0
- package/src/BottomSheet/components/BottomSheetOption/index.ts +1 -0
- package/src/BottomSheet/index.ts +1 -0
- package/src/BottomSheet/messages.ts +9 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useIsScreenReaderEnabled.ts +32 -0
- package/src/index.ts +1 -1
- package/src/utils/test/wait.ts +52 -0
- package/dist/src/TextList/TextList.js +0 -13
- package/dist/src/TextList/TextList.style.js +0 -16
- package/dist/src/TextList/index.js +0 -1
- package/dist/types/src/TextList/TextList.d.ts +0 -30
- package/dist/types/src/TextList/TextList.style.d.ts +0 -14
- package/dist/types/src/TextList/index.d.ts +0 -1
- package/src/TextList/TextList.style.ts +0 -17
- package/src/TextList/TextList.test.tsx +0 -20
- package/src/TextList/TextList.tsx +0 -68
- package/src/TextList/index.ts +0 -1
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import React, { ReactNode } from "react";
|
|
2
|
+
import { Modalize } from "react-native-modalize";
|
|
3
|
+
interface BottomSheetProps {
|
|
4
|
+
readonly children: ReactNode;
|
|
5
|
+
/**
|
|
6
|
+
* Display a cancel button in the bottom sheet footer.
|
|
7
|
+
*/
|
|
8
|
+
readonly showCancel?: boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Hide or show the cancel button when loading state is provided.
|
|
11
|
+
*/
|
|
12
|
+
readonly loading?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* An optional heading to display in the bottom sheet header.
|
|
15
|
+
*/
|
|
16
|
+
readonly heading?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Callback that is called when the overlay is opened.
|
|
19
|
+
*/
|
|
20
|
+
readonly onOpen?: () => void;
|
|
21
|
+
/**
|
|
22
|
+
* Callback that is called when the overlay is closed.
|
|
23
|
+
*/
|
|
24
|
+
readonly onClose?: () => void;
|
|
25
|
+
}
|
|
26
|
+
export declare const BottomSheet: React.ForwardRefExoticComponent<BottomSheetProps & React.RefAttributes<import("react-native-modalize/lib/options").IHandles | undefined>>;
|
|
27
|
+
export type BottomSheetRef = Modalize | undefined;
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export declare const styles: {
|
|
2
|
+
overlayModalize: {
|
|
3
|
+
backgroundColor: string;
|
|
4
|
+
};
|
|
5
|
+
overlay: {
|
|
6
|
+
backgroundColor: string;
|
|
7
|
+
height: number;
|
|
8
|
+
position: "absolute";
|
|
9
|
+
left: 0;
|
|
10
|
+
right: 0;
|
|
11
|
+
top: 0;
|
|
12
|
+
bottom: 0;
|
|
13
|
+
};
|
|
14
|
+
modal: {
|
|
15
|
+
borderTopLeftRadius: number;
|
|
16
|
+
borderTopRightRadius: number;
|
|
17
|
+
paddingTop: number;
|
|
18
|
+
};
|
|
19
|
+
children: {
|
|
20
|
+
paddingBottom: number;
|
|
21
|
+
};
|
|
22
|
+
header: {
|
|
23
|
+
paddingHorizontal: number;
|
|
24
|
+
paddingTop: number;
|
|
25
|
+
paddingBottom: number;
|
|
26
|
+
};
|
|
27
|
+
footerDivider: {
|
|
28
|
+
marginHorizontal: number;
|
|
29
|
+
marginTop: number;
|
|
30
|
+
marginBottom: number;
|
|
31
|
+
};
|
|
32
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
import { IconColorNames, IconNames } from "@jobber/design";
|
|
3
|
+
import { TextAlign } from "../../../Typography";
|
|
4
|
+
export interface BottomSheetOptionProps {
|
|
5
|
+
readonly text: string;
|
|
6
|
+
readonly icon?: IconNames;
|
|
7
|
+
readonly iconColor?: IconColorNames;
|
|
8
|
+
readonly textAlign?: TextAlign;
|
|
9
|
+
readonly destructive?: boolean;
|
|
10
|
+
readonly textTransform?: "none" | "capitalize";
|
|
11
|
+
onPress: () => void;
|
|
12
|
+
}
|
|
13
|
+
export declare function BottomSheetOption({ text, icon, iconColor, textAlign, destructive, textTransform, onPress, }: BottomSheetOptionProps): JSX.Element;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare const styles: {
|
|
2
|
+
bottomSheetOption: {
|
|
3
|
+
display: "flex";
|
|
4
|
+
flexDirection: "row";
|
|
5
|
+
alignContent: "center";
|
|
6
|
+
alignItems: "center";
|
|
7
|
+
padding: number;
|
|
8
|
+
};
|
|
9
|
+
icon: {
|
|
10
|
+
paddingHorizontal: number;
|
|
11
|
+
};
|
|
12
|
+
title: {
|
|
13
|
+
paddingHorizontal: number;
|
|
14
|
+
flexShrink: number;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { BottomSheetOption, BottomSheetOptionProps } from "./BottomSheetOption";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { BottomSheet } from "./BottomSheet";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function useIsScreenReaderEnabled(): boolean;
|
|
@@ -2,6 +2,7 @@ export * from "./ActionItem";
|
|
|
2
2
|
export * from "./ActionLabel";
|
|
3
3
|
export * from "./ActivityIndicator";
|
|
4
4
|
export * from "./AtlantisContext";
|
|
5
|
+
export * from "./BottomSheet";
|
|
5
6
|
export * from "./Button";
|
|
6
7
|
export * from "./Card";
|
|
7
8
|
export * from "./Chip";
|
|
@@ -16,7 +17,6 @@ export * from "./IconButton";
|
|
|
16
17
|
export * from "./InputFieldWrapper";
|
|
17
18
|
export * from "./InputPressable";
|
|
18
19
|
export * from "./InputText";
|
|
19
|
-
export * from "./TextList";
|
|
20
20
|
export * from "./ProgressBar";
|
|
21
21
|
export * from "./StatusLabel";
|
|
22
22
|
export * from "./Switch";
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* wait is used in our tests for testing Apollo Mocks
|
|
3
|
+
* useQuery returns fetching equal to true in the first render
|
|
4
|
+
* and the second render would return the mock result
|
|
5
|
+
* using "await act(wait)", we can wait for the next rerender
|
|
6
|
+
* to get the mock result of the query
|
|
7
|
+
* @param milliseconds time to wait in milliseconds
|
|
8
|
+
*/
|
|
9
|
+
export declare function wait(milliseconds?: number): Promise<void>;
|
|
10
|
+
/**
|
|
11
|
+
* Use this test helper thoughtfully. If you are running into the
|
|
12
|
+
* "not wrapped in act(...)" warning while running your test it is
|
|
13
|
+
* recommended that:
|
|
14
|
+
*
|
|
15
|
+
* 1. Double check that there are really no layout changes that you can test against.
|
|
16
|
+
* waitForUntestableRender will fail your test if it detects layout changes
|
|
17
|
+
* 2. Re-evalutate your implementation. Maybe the managed state can be deferred to
|
|
18
|
+
* to a component further down the hierarchy? This can help prevent renders since
|
|
19
|
+
* your managed state isn't being used yet maybe?
|
|
20
|
+
*
|
|
21
|
+
* `waitForUntestableRender` is meant to be used as an alternative to
|
|
22
|
+
*
|
|
23
|
+
* ```tsx
|
|
24
|
+
* await act(wait)
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* to make tests more readable.
|
|
28
|
+
*
|
|
29
|
+
* The `explanation` argument is required to ensure that a developer _thinks_
|
|
30
|
+
* about why they need use this function. It is _really_ important that devs
|
|
31
|
+
* understand when _and why_ a component's render cycle is flagged to be untested
|
|
32
|
+
* So do not put a throwaway explanation here. If you don't know why your test
|
|
33
|
+
* needs this function, find out!
|
|
34
|
+
|
|
35
|
+
*/
|
|
36
|
+
export declare const waitForUntestableRender: (_explanation: string) => Promise<void>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jobber/components-native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.1-fix-breakp.0+f1ad84f5",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"main": "dist/src/index.js",
|
|
6
6
|
"module": "dist/src/index.js",
|
|
@@ -21,15 +21,17 @@
|
|
|
21
21
|
"build:clean": "rm -rf ./dist"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@jobber/design": "^0.41.
|
|
24
|
+
"@jobber/design": "^0.41.3-fix-breakp.14+f1ad84f5",
|
|
25
25
|
"lodash.chunk": "^4.2.0",
|
|
26
26
|
"lodash.identity": "^3.0.0",
|
|
27
27
|
"react-hook-form": "^7.30.0",
|
|
28
28
|
"react-intl": "^6.4.2",
|
|
29
|
-
"react-native-gesture-handler": "^2.
|
|
29
|
+
"react-native-gesture-handler": "^2.10.2",
|
|
30
30
|
"react-native-localize": "^2.2.6",
|
|
31
|
+
"react-native-modalize": "^2.0.13",
|
|
32
|
+
"react-native-portalize": "^1.0.7",
|
|
31
33
|
"react-native-reanimated": "^2.17.0",
|
|
32
|
-
"react-native-safe-area-context": "^4.5.
|
|
34
|
+
"react-native-safe-area-context": "^4.5.3",
|
|
33
35
|
"react-native-svg": "^13.9.0",
|
|
34
36
|
"react-native-toast-message": "^2.1.6",
|
|
35
37
|
"react-native-uuid": "^1.4.9",
|
|
@@ -53,5 +55,5 @@
|
|
|
53
55
|
"react": "^18",
|
|
54
56
|
"react-native": ">=0.69.2"
|
|
55
57
|
},
|
|
56
|
-
"gitHead": "
|
|
58
|
+
"gitHead": "f1ad84f5e1d4651bce5e755535c7ae6276033ab8"
|
|
57
59
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Dimensions, StyleSheet } from "react-native";
|
|
2
|
+
import { tokens } from "../utils/design";
|
|
3
|
+
|
|
4
|
+
const { height } = Dimensions.get("window");
|
|
5
|
+
const modalBorderRadius = tokens["radius-larger"];
|
|
6
|
+
|
|
7
|
+
export const styles = StyleSheet.create({
|
|
8
|
+
overlayModalize: {
|
|
9
|
+
backgroundColor: "transparent",
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
overlay: {
|
|
13
|
+
...StyleSheet.absoluteFillObject,
|
|
14
|
+
backgroundColor: tokens["color-overlay"],
|
|
15
|
+
height,
|
|
16
|
+
},
|
|
17
|
+
modal: {
|
|
18
|
+
borderTopLeftRadius: modalBorderRadius,
|
|
19
|
+
borderTopRightRadius: modalBorderRadius,
|
|
20
|
+
paddingTop: tokens["space-small"],
|
|
21
|
+
},
|
|
22
|
+
children: {
|
|
23
|
+
paddingBottom: tokens["space-small"],
|
|
24
|
+
},
|
|
25
|
+
header: {
|
|
26
|
+
paddingHorizontal: tokens["space-base"],
|
|
27
|
+
paddingTop: tokens["space-small"],
|
|
28
|
+
paddingBottom: tokens["space-base"],
|
|
29
|
+
},
|
|
30
|
+
footerDivider: {
|
|
31
|
+
marginHorizontal: tokens["space-base"],
|
|
32
|
+
marginTop: tokens["space-small"],
|
|
33
|
+
marginBottom: tokens["space-smaller"],
|
|
34
|
+
},
|
|
35
|
+
});
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import React, { createRef } from "react";
|
|
2
|
+
import { act, fireEvent, render, waitFor } from "@testing-library/react-native";
|
|
3
|
+
import { AccessibilityInfo, View } from "react-native";
|
|
4
|
+
import { Host, Portal } from "react-native-portalize";
|
|
5
|
+
import { BottomSheet } from ".";
|
|
6
|
+
import { BottomSheetRef } from "./BottomSheet";
|
|
7
|
+
import { messages } from "./messages";
|
|
8
|
+
import { waitForUntestableRender } from "../utils/test/wait";
|
|
9
|
+
import { Text } from "../Text";
|
|
10
|
+
|
|
11
|
+
jest.unmock("../hooks/useIsScreenReaderEnabled");
|
|
12
|
+
|
|
13
|
+
const ref = createRef<BottomSheetRef>();
|
|
14
|
+
const mockOnClose = jest.fn();
|
|
15
|
+
const mockOnOpen = jest.fn();
|
|
16
|
+
|
|
17
|
+
function setup({
|
|
18
|
+
heading,
|
|
19
|
+
showCancel,
|
|
20
|
+
loading,
|
|
21
|
+
}: {
|
|
22
|
+
heading?: string;
|
|
23
|
+
showCancel?: boolean;
|
|
24
|
+
loading?: boolean;
|
|
25
|
+
}) {
|
|
26
|
+
return render(
|
|
27
|
+
<Host>
|
|
28
|
+
<Portal>
|
|
29
|
+
<BottomSheet
|
|
30
|
+
ref={ref}
|
|
31
|
+
heading={heading}
|
|
32
|
+
showCancel={showCancel}
|
|
33
|
+
loading={loading}
|
|
34
|
+
onClose={mockOnClose}
|
|
35
|
+
onOpen={mockOnOpen}
|
|
36
|
+
>
|
|
37
|
+
<View>
|
|
38
|
+
<Text>BottomSheet</Text>
|
|
39
|
+
</View>
|
|
40
|
+
</BottomSheet>
|
|
41
|
+
</Portal>
|
|
42
|
+
</Host>,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
it("renders a BottomSheet", async () => {
|
|
47
|
+
const { getByText } = setup({});
|
|
48
|
+
|
|
49
|
+
await waitForUntestableRender(
|
|
50
|
+
"Wait for AccessibilityInfo.isScreenReaderEnabled to resolve",
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
act(() => {
|
|
54
|
+
ref.current?.open();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
expect(getByText("BottomSheet")).toBeDefined();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("renders a BottomSheet with a header", async () => {
|
|
61
|
+
const header = "Hello this is header";
|
|
62
|
+
const { getByText } = setup({ heading: header });
|
|
63
|
+
|
|
64
|
+
await waitForUntestableRender(
|
|
65
|
+
"Wait for AccessibilityInfo.isScreenReaderEnabled to resolve",
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
act(() => {
|
|
69
|
+
ref.current?.open();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
expect(getByText(header)).toBeDefined();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("BottomSheet can be closed with the cancel action", async () => {
|
|
76
|
+
const { getByText, queryByText } = setup({ showCancel: true });
|
|
77
|
+
|
|
78
|
+
act(() => {
|
|
79
|
+
ref.current?.open();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
fireEvent.press(getByText(messages.cancel.defaultMessage));
|
|
83
|
+
|
|
84
|
+
await waitFor(() => {
|
|
85
|
+
expect(queryByText("BottomSheet")).toBeNull();
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe("when loading is provided and true", () => {
|
|
90
|
+
it("hides the cancel action", async () => {
|
|
91
|
+
const { queryByText } = setup({ showCancel: true, loading: true });
|
|
92
|
+
|
|
93
|
+
await waitForUntestableRender(
|
|
94
|
+
"Wait for AccessibilityInfo.isScreenReaderEnabled to resolve",
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
act(() => {
|
|
98
|
+
ref.current?.open();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
expect(queryByText(messages.cancel.defaultMessage)).toBeNull();
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("calls onClose when BottomSheet is closed", async () => {
|
|
106
|
+
setup({});
|
|
107
|
+
|
|
108
|
+
act(() => {
|
|
109
|
+
ref.current?.open();
|
|
110
|
+
ref.current?.close();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
await waitFor(() => {
|
|
114
|
+
expect(mockOnClose).toHaveBeenCalled();
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("calls onOpen when BottomSheet is opened", async () => {
|
|
119
|
+
setup({});
|
|
120
|
+
|
|
121
|
+
act(() => {
|
|
122
|
+
ref.current?.open();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
await waitFor(() => {
|
|
126
|
+
expect(mockOnOpen).toHaveBeenCalled();
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe("when there is a screen reader enabled", () => {
|
|
131
|
+
it("should always show the cancel action", async () => {
|
|
132
|
+
jest
|
|
133
|
+
.spyOn(AccessibilityInfo, "isScreenReaderEnabled")
|
|
134
|
+
.mockImplementation(() => Promise.resolve(true));
|
|
135
|
+
|
|
136
|
+
const { getByText, queryByText } = setup({});
|
|
137
|
+
|
|
138
|
+
await waitForUntestableRender(
|
|
139
|
+
"Wait for AccessibilityInfo.isScreenReaderEnabled to resolve",
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
act(() => {
|
|
143
|
+
ref.current?.open();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
fireEvent.press(getByText(messages.cancel.defaultMessage));
|
|
147
|
+
|
|
148
|
+
await waitFor(() => {
|
|
149
|
+
expect(queryByText("BottomSheet")).toBeNull();
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
});
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import React, { ReactNode, Ref, RefObject, forwardRef, useState } from "react";
|
|
2
|
+
import { Modalize } from "react-native-modalize";
|
|
3
|
+
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
4
|
+
import { Keyboard, View } from "react-native";
|
|
5
|
+
import { useIntl } from "react-intl";
|
|
6
|
+
import { BottomSheetOption } from "./components/BottomSheetOption";
|
|
7
|
+
import { styles } from "./BottomSheet.style";
|
|
8
|
+
import { messages } from "./messages";
|
|
9
|
+
import { useIsScreenReaderEnabled } from "../hooks";
|
|
10
|
+
import { Divider } from "../Divider";
|
|
11
|
+
import { Heading } from "../Heading";
|
|
12
|
+
|
|
13
|
+
interface BottomSheetProps {
|
|
14
|
+
readonly children: ReactNode;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Display a cancel button in the bottom sheet footer.
|
|
18
|
+
*/
|
|
19
|
+
readonly showCancel?: boolean;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Hide or show the cancel button when loading state is provided.
|
|
23
|
+
*/
|
|
24
|
+
readonly loading?: boolean;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* An optional heading to display in the bottom sheet header.
|
|
28
|
+
*/
|
|
29
|
+
readonly heading?: string;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Callback that is called when the overlay is opened.
|
|
33
|
+
*/
|
|
34
|
+
readonly onOpen?: () => void;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Callback that is called when the overlay is closed.
|
|
38
|
+
*/
|
|
39
|
+
readonly onClose?: () => void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const BottomSheet = forwardRef(BottomSheetInternal);
|
|
43
|
+
|
|
44
|
+
export type BottomSheetRef = Modalize | undefined;
|
|
45
|
+
|
|
46
|
+
function BottomSheetInternal(
|
|
47
|
+
{
|
|
48
|
+
children,
|
|
49
|
+
showCancel,
|
|
50
|
+
loading = false,
|
|
51
|
+
heading,
|
|
52
|
+
onOpen,
|
|
53
|
+
onClose,
|
|
54
|
+
}: BottomSheetProps,
|
|
55
|
+
ref: Ref<BottomSheetRef>,
|
|
56
|
+
) {
|
|
57
|
+
const isScreenReaderEnabled = useIsScreenReaderEnabled();
|
|
58
|
+
const [open, setOpen] = useState<boolean>(false);
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<>
|
|
62
|
+
{open && <Overlay />}
|
|
63
|
+
<Modalize
|
|
64
|
+
ref={ref}
|
|
65
|
+
adjustToContentHeight={true}
|
|
66
|
+
modalStyle={styles.modal}
|
|
67
|
+
overlayStyle={styles.overlayModalize}
|
|
68
|
+
HeaderComponent={heading && <Header heading={heading} />}
|
|
69
|
+
FooterComponent={
|
|
70
|
+
<Footer
|
|
71
|
+
cancellable={(showCancel && !loading) || isScreenReaderEnabled}
|
|
72
|
+
onCancel={() => {
|
|
73
|
+
(ref as RefObject<BottomSheetRef>)?.current?.close();
|
|
74
|
+
}}
|
|
75
|
+
/>
|
|
76
|
+
}
|
|
77
|
+
withHandle={false}
|
|
78
|
+
withReactModal={isScreenReaderEnabled}
|
|
79
|
+
onOpen={openModal}
|
|
80
|
+
onClose={closeModal}
|
|
81
|
+
>
|
|
82
|
+
<View
|
|
83
|
+
style={
|
|
84
|
+
!showCancel && !isScreenReaderEnabled ? styles.children : undefined
|
|
85
|
+
}
|
|
86
|
+
>
|
|
87
|
+
{children}
|
|
88
|
+
</View>
|
|
89
|
+
</Modalize>
|
|
90
|
+
</>
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
function openModal() {
|
|
94
|
+
onOpen?.();
|
|
95
|
+
setOpen(true);
|
|
96
|
+
dismissKeyboard();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function closeModal() {
|
|
100
|
+
onClose?.();
|
|
101
|
+
setOpen(false);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function Header({ heading }: { heading: string }) {
|
|
106
|
+
return (
|
|
107
|
+
<View style={styles.header}>
|
|
108
|
+
<Heading level={"subtitle"}>{heading}</Heading>
|
|
109
|
+
</View>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function Footer({
|
|
114
|
+
cancellable,
|
|
115
|
+
onCancel,
|
|
116
|
+
}: {
|
|
117
|
+
cancellable: boolean;
|
|
118
|
+
onCancel: () => void;
|
|
119
|
+
}) {
|
|
120
|
+
const insets = useSafeAreaInsets();
|
|
121
|
+
const { formatMessage } = useIntl();
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<View style={{ marginBottom: insets.bottom }}>
|
|
125
|
+
{cancellable && (
|
|
126
|
+
<View style={styles.children}>
|
|
127
|
+
<View style={styles.footerDivider}>
|
|
128
|
+
<Divider />
|
|
129
|
+
</View>
|
|
130
|
+
<BottomSheetOption
|
|
131
|
+
text={formatMessage(messages.cancel)}
|
|
132
|
+
icon={"remove"}
|
|
133
|
+
onPress={onCancel}
|
|
134
|
+
/>
|
|
135
|
+
</View>
|
|
136
|
+
)}
|
|
137
|
+
</View>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function dismissKeyboard() {
|
|
142
|
+
//Dismisses the keyboard before opening the bottom sheet.
|
|
143
|
+
//In the case where an input text field is focused we don't want to show the bottom sheet behind or above keyboard
|
|
144
|
+
Keyboard.dismiss();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function Overlay() {
|
|
148
|
+
return <View style={styles.overlay} />;
|
|
149
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { StyleSheet } from "react-native";
|
|
2
|
+
import { tokens } from "../../../utils/design";
|
|
3
|
+
|
|
4
|
+
export const styles = StyleSheet.create({
|
|
5
|
+
bottomSheetOption: {
|
|
6
|
+
display: "flex",
|
|
7
|
+
flexDirection: "row",
|
|
8
|
+
alignContent: "center",
|
|
9
|
+
alignItems: "center",
|
|
10
|
+
padding: tokens["space-small"],
|
|
11
|
+
},
|
|
12
|
+
icon: {
|
|
13
|
+
paddingHorizontal: tokens["space-small"],
|
|
14
|
+
},
|
|
15
|
+
title: {
|
|
16
|
+
paddingHorizontal: tokens["space-small"],
|
|
17
|
+
flexShrink: 1,
|
|
18
|
+
},
|
|
19
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { fireEvent, render } from "@testing-library/react-native";
|
|
3
|
+
import { BottomSheetOption } from "./BottomSheetOption";
|
|
4
|
+
|
|
5
|
+
describe("BottomSheetOption component", () => {
|
|
6
|
+
it("renders", () => {
|
|
7
|
+
const mockNavigate = jest.fn();
|
|
8
|
+
const iconName = "camera";
|
|
9
|
+
const optionLabel = "Take photo";
|
|
10
|
+
|
|
11
|
+
const { getByTestId, getByText } = render(
|
|
12
|
+
<BottomSheetOption
|
|
13
|
+
onPress={mockNavigate}
|
|
14
|
+
icon={iconName}
|
|
15
|
+
text={optionLabel}
|
|
16
|
+
/>,
|
|
17
|
+
);
|
|
18
|
+
expect(getByTestId(iconName)).toBeDefined();
|
|
19
|
+
expect(getByText(optionLabel)).toBeDefined();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("responds to presses", () => {
|
|
23
|
+
const mockNavigate = jest.fn();
|
|
24
|
+
const tree = render(
|
|
25
|
+
<BottomSheetOption
|
|
26
|
+
onPress={mockNavigate}
|
|
27
|
+
icon={"camera"}
|
|
28
|
+
text={"Take photo"}
|
|
29
|
+
/>,
|
|
30
|
+
);
|
|
31
|
+
fireEvent.press(tree.getByLabelText("Take photo"));
|
|
32
|
+
expect(mockNavigate).toHaveBeenCalled();
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { TouchableOpacity, View } from "react-native";
|
|
3
|
+
import { IconColorNames, IconNames } from "@jobber/design";
|
|
4
|
+
import { styles } from "./BottomSheetOption.styles";
|
|
5
|
+
import { TextAlign } from "../../../Typography";
|
|
6
|
+
import { capitalize } from "../../../utils/intl";
|
|
7
|
+
import { Text } from "../../../Text";
|
|
8
|
+
import { Icon } from "../../../Icon";
|
|
9
|
+
|
|
10
|
+
export interface BottomSheetOptionProps {
|
|
11
|
+
readonly text: string;
|
|
12
|
+
readonly icon?: IconNames;
|
|
13
|
+
readonly iconColor?: IconColorNames;
|
|
14
|
+
readonly textAlign?: TextAlign;
|
|
15
|
+
readonly destructive?: boolean;
|
|
16
|
+
readonly textTransform?: "none" | "capitalize";
|
|
17
|
+
onPress: () => void;
|
|
18
|
+
}
|
|
19
|
+
export function BottomSheetOption({
|
|
20
|
+
text,
|
|
21
|
+
icon,
|
|
22
|
+
iconColor,
|
|
23
|
+
textAlign,
|
|
24
|
+
destructive,
|
|
25
|
+
textTransform = "capitalize",
|
|
26
|
+
onPress,
|
|
27
|
+
}: BottomSheetOptionProps): JSX.Element {
|
|
28
|
+
const destructiveColor = "critical";
|
|
29
|
+
const textVariation = destructive ? destructiveColor : "subdued";
|
|
30
|
+
return (
|
|
31
|
+
<TouchableOpacity
|
|
32
|
+
style={styles.bottomSheetOption}
|
|
33
|
+
onPress={onPress}
|
|
34
|
+
accessibilityLabel={text}
|
|
35
|
+
>
|
|
36
|
+
{icon && (
|
|
37
|
+
<View style={styles.icon}>
|
|
38
|
+
<Icon
|
|
39
|
+
name={icon}
|
|
40
|
+
color={destructive ? destructiveColor : iconColor}
|
|
41
|
+
/>
|
|
42
|
+
</View>
|
|
43
|
+
)}
|
|
44
|
+
<View style={styles.title}>
|
|
45
|
+
<Text variation={textVariation} emphasis={"strong"} align={textAlign}>
|
|
46
|
+
{textTransform === "capitalize"
|
|
47
|
+
? capitalize(text.toLocaleLowerCase())
|
|
48
|
+
: text}
|
|
49
|
+
</Text>
|
|
50
|
+
</View>
|
|
51
|
+
</TouchableOpacity>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { BottomSheetOption, BottomSheetOptionProps } from "./BottomSheetOption";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { BottomSheet } from "./BottomSheet";
|
package/src/hooks/index.ts
CHANGED