@jobber/components-native 0.26.0 → 0.27.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/ButtonGroup/ButtonGroup.js +29 -0
- package/dist/src/ButtonGroup/ButtonGroup.style.js +18 -0
- package/dist/src/ButtonGroup/ButtonGroupAction.js +10 -0
- package/dist/src/ButtonGroup/components/SecondaryActionSheet/SecondaryActionSheet.js +17 -0
- package/dist/src/ButtonGroup/components/SecondaryActionSheet/index.js +1 -0
- package/dist/src/ButtonGroup/index.js +2 -0
- package/dist/src/ButtonGroup/messages.js +18 -0
- package/dist/src/ButtonGroup/types.js +1 -0
- package/dist/src/ButtonGroup/utils.js +42 -0
- package/dist/src/index.js +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/src/ButtonGroup/ButtonGroup.d.ts +30 -0
- package/dist/types/src/ButtonGroup/ButtonGroup.style.d.ts +16 -0
- package/dist/types/src/ButtonGroup/ButtonGroupAction.d.ts +47 -0
- package/dist/types/src/ButtonGroup/components/SecondaryActionSheet/SecondaryActionSheet.d.ts +13 -0
- package/dist/types/src/ButtonGroup/components/SecondaryActionSheet/index.d.ts +1 -0
- package/dist/types/src/ButtonGroup/index.d.ts +3 -0
- package/dist/types/src/ButtonGroup/messages.d.ts +17 -0
- package/dist/types/src/ButtonGroup/types.d.ts +7 -0
- package/dist/types/src/ButtonGroup/utils.d.ts +19 -0
- package/dist/types/src/index.d.ts +1 -0
- package/package.json +2 -2
- package/src/ButtonGroup/ButtonGroup.style.ts +19 -0
- package/src/ButtonGroup/ButtonGroup.test.tsx +325 -0
- package/src/ButtonGroup/ButtonGroup.tsx +113 -0
- package/src/ButtonGroup/ButtonGroupAction.tsx +66 -0
- package/src/ButtonGroup/components/SecondaryActionSheet/SecondaryActionSheet.tsx +55 -0
- package/src/ButtonGroup/components/SecondaryActionSheet/index.ts +1 -0
- package/src/ButtonGroup/index.ts +8 -0
- package/src/ButtonGroup/messages.ts +19 -0
- package/src/ButtonGroup/types.ts +30 -0
- package/src/ButtonGroup/utils.ts +86 -0
- package/src/index.ts +1 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
import { ButtonGroupActionElement } from "./types";
|
|
3
|
+
export interface ButtonGroupProps {
|
|
4
|
+
readonly children: ButtonGroupActionElement | ButtonGroupActionElement[];
|
|
5
|
+
/**
|
|
6
|
+
* Display a cancel button in the secondary bottom sheet footer.
|
|
7
|
+
*/
|
|
8
|
+
readonly showCancelInBottomSheet?: boolean;
|
|
9
|
+
/**
|
|
10
|
+
* An optional heading to display in the secondary bottom sheet header.
|
|
11
|
+
*/
|
|
12
|
+
readonly bottomSheetHeading?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Callback that is called when the secondary actions bottom sheet is opened.
|
|
15
|
+
*/
|
|
16
|
+
readonly onOpenBottomSheet?: () => void;
|
|
17
|
+
/**
|
|
18
|
+
* Callback that is called when the secondary actions bottom sheet is closed.
|
|
19
|
+
*/
|
|
20
|
+
readonly onCloseBottomSheet?: () => void;
|
|
21
|
+
/**
|
|
22
|
+
* Allows you to Tap the button while offline
|
|
23
|
+
*/
|
|
24
|
+
readonly allowTapWhenOffline?: boolean;
|
|
25
|
+
}
|
|
26
|
+
export declare function ButtonGroup({ children, showCancelInBottomSheet, bottomSheetHeading, onOpenBottomSheet, onCloseBottomSheet, allowTapWhenOffline, }: ButtonGroupProps): JSX.Element;
|
|
27
|
+
export declare namespace ButtonGroup {
|
|
28
|
+
var PrimaryAction: typeof import("./ButtonGroupAction").PrimaryAction;
|
|
29
|
+
var SecondaryAction: typeof import("./ButtonGroupAction").SecondaryAction;
|
|
30
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare const styles: {
|
|
2
|
+
buttonGroup: {
|
|
3
|
+
width: string;
|
|
4
|
+
flexDirection: "row";
|
|
5
|
+
justifyContent: "flex-end";
|
|
6
|
+
};
|
|
7
|
+
button: {
|
|
8
|
+
flexBasis: number;
|
|
9
|
+
flexGrow: number;
|
|
10
|
+
paddingRight: number;
|
|
11
|
+
};
|
|
12
|
+
moreButton: {
|
|
13
|
+
flexBasis: number;
|
|
14
|
+
flexGrow: number;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
import { IconColorNames, IconNames } from "@jobber/design";
|
|
3
|
+
import { ButtonType, ButtonVariation } from "../Button";
|
|
4
|
+
export interface ButtonGroupActionProps {
|
|
5
|
+
/**
|
|
6
|
+
* Text to be displayed on the action button
|
|
7
|
+
*/
|
|
8
|
+
label: string;
|
|
9
|
+
/**
|
|
10
|
+
* Icon to be displayed on the action button
|
|
11
|
+
*/
|
|
12
|
+
icon?: IconNames;
|
|
13
|
+
/**
|
|
14
|
+
* Determines the color of the icon. If not specified, some icons have a default system colour which will be used
|
|
15
|
+
* Others that don't have a system colour fall back to greyBlue.
|
|
16
|
+
* Only applies the iconColor to ButtonGroup.PrimaryAction when it is rendered under the "more" menu.
|
|
17
|
+
*/
|
|
18
|
+
iconColor?: IconColorNames;
|
|
19
|
+
/**
|
|
20
|
+
* Press handler for the action button
|
|
21
|
+
*/
|
|
22
|
+
onPress: () => void;
|
|
23
|
+
loading?: boolean;
|
|
24
|
+
}
|
|
25
|
+
export interface ButtonGroupSecondaryActionProps extends ButtonGroupActionProps {
|
|
26
|
+
/**
|
|
27
|
+
* Indicates whether the secondary action is destructive in nature.
|
|
28
|
+
*/
|
|
29
|
+
destructive?: boolean;
|
|
30
|
+
}
|
|
31
|
+
export interface ButtonGroupPrimaryActionProps extends ButtonGroupActionProps {
|
|
32
|
+
/**
|
|
33
|
+
* Sets the action button style (default: "primary")
|
|
34
|
+
*/
|
|
35
|
+
buttonType?: ButtonType;
|
|
36
|
+
/**
|
|
37
|
+
* Themes the action button to the type of action it performs (default: "work")
|
|
38
|
+
*/
|
|
39
|
+
buttonVariation?: ButtonVariation;
|
|
40
|
+
/**
|
|
41
|
+
* Optional custom button that can be rendered in place of the primary action button
|
|
42
|
+
*/
|
|
43
|
+
customButton?: JSX.Element;
|
|
44
|
+
loading?: boolean;
|
|
45
|
+
}
|
|
46
|
+
export declare function PrimaryAction(_: ButtonGroupPrimaryActionProps): JSX.Element;
|
|
47
|
+
export declare function SecondaryAction(_: ButtonGroupSecondaryActionProps): JSX.Element;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { RefObject } from "react";
|
|
2
|
+
import { ButtonGroupSecondaryActionProps } from "../../types";
|
|
3
|
+
import { BottomSheetRef } from "../../../BottomSheet/BottomSheet";
|
|
4
|
+
interface SecondaryActionSheetProps {
|
|
5
|
+
actions: ButtonGroupSecondaryActionProps[];
|
|
6
|
+
secondaryActionsRef: RefObject<BottomSheetRef>;
|
|
7
|
+
showCancel?: boolean;
|
|
8
|
+
heading?: string;
|
|
9
|
+
onOpenBottomSheet?: () => void;
|
|
10
|
+
onCloseBottomSheet?: () => void;
|
|
11
|
+
}
|
|
12
|
+
export declare function SecondaryActionSheet({ actions, secondaryActionsRef, heading, showCancel, onOpenBottomSheet, onCloseBottomSheet, }: SecondaryActionSheetProps): JSX.Element;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { SecondaryActionSheet } from "./SecondaryActionSheet";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare const messages: {
|
|
2
|
+
more: {
|
|
3
|
+
id: string;
|
|
4
|
+
defaultMessage: string;
|
|
5
|
+
description: string;
|
|
6
|
+
};
|
|
7
|
+
unavailableNetworkTitle: {
|
|
8
|
+
id: string;
|
|
9
|
+
defaultMessage: string;
|
|
10
|
+
description: string;
|
|
11
|
+
};
|
|
12
|
+
unavailableNetworkMessage: {
|
|
13
|
+
id: string;
|
|
14
|
+
defaultMessage: string;
|
|
15
|
+
description: string;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { ReactElement } from "react";
|
|
2
|
+
import { ButtonGroupActionProps, ButtonGroupPrimaryActionProps, ButtonGroupSecondaryActionProps, PrimaryAction, SecondaryAction } from "./ButtonGroupAction";
|
|
3
|
+
export type ButtonGroupPrimaryActionElement = ReactElement<ButtonGroupPrimaryActionProps, typeof PrimaryAction>;
|
|
4
|
+
export type ButtonGroupSecondaryActionElement = ReactElement<ButtonGroupActionProps, typeof SecondaryAction>;
|
|
5
|
+
export type ButtonGroupActionElement = ButtonGroupPrimaryActionElement | ButtonGroupSecondaryActionElement;
|
|
6
|
+
export type BottomSheetTextTransform = "capitalize" | "none";
|
|
7
|
+
export type { ButtonGroupActionProps, ButtonGroupPrimaryActionProps, ButtonGroupSecondaryActionProps, };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ButtonGroupActionElement, ButtonGroupPrimaryActionElement } from "./types";
|
|
2
|
+
import { ButtonGroupActionProps } from "./ButtonGroupAction";
|
|
3
|
+
interface UsePreventTapWhenOfflineShape {
|
|
4
|
+
readonly handlePress: (callback: ButtonGroupActionProps["onPress"]) => () => void;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Determine if the onPress should be fired or an alert when the device is
|
|
8
|
+
* online or offline
|
|
9
|
+
*/
|
|
10
|
+
export declare function usePreventTapWhenOffline(): UsePreventTapWhenOfflineShape;
|
|
11
|
+
interface GetActionShape {
|
|
12
|
+
readonly primaryActions: ButtonGroupPrimaryActionElement[];
|
|
13
|
+
readonly secondaryActions: ButtonGroupActionElement[];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Separate the Primary and Secondary actions into 2 const
|
|
17
|
+
*/
|
|
18
|
+
export declare function getActions(children: ButtonGroupActionElement | ButtonGroupActionElement[]): GetActionShape;
|
|
19
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jobber/components-native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"main": "dist/src/index.js",
|
|
6
6
|
"module": "dist/src/index.js",
|
|
@@ -55,5 +55,5 @@
|
|
|
55
55
|
"react": "^18",
|
|
56
56
|
"react-native": ">=0.69.2"
|
|
57
57
|
},
|
|
58
|
-
"gitHead": "
|
|
58
|
+
"gitHead": "4cac408646d885509b88823f5186df1a9534b641"
|
|
59
59
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { StyleSheet } from "react-native";
|
|
2
|
+
import { tokens } from "../utils/design";
|
|
3
|
+
|
|
4
|
+
export const styles = StyleSheet.create({
|
|
5
|
+
buttonGroup: {
|
|
6
|
+
width: "100%",
|
|
7
|
+
flexDirection: "row",
|
|
8
|
+
justifyContent: "flex-end",
|
|
9
|
+
},
|
|
10
|
+
button: {
|
|
11
|
+
flexBasis: tokens["space-largest"],
|
|
12
|
+
flexGrow: 1,
|
|
13
|
+
paddingRight: tokens["space-smaller"],
|
|
14
|
+
},
|
|
15
|
+
moreButton: {
|
|
16
|
+
flexBasis: tokens["space-largest"],
|
|
17
|
+
flexGrow: 0,
|
|
18
|
+
},
|
|
19
|
+
});
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { fireEvent, render } from "@testing-library/react-native";
|
|
3
|
+
import { Host } from "react-native-portalize";
|
|
4
|
+
import { Alert } from "react-native";
|
|
5
|
+
import { messages } from "./messages";
|
|
6
|
+
import { ButtonGroup, ButtonGroupProps } from "./ButtonGroup";
|
|
7
|
+
import { Button } from "../Button";
|
|
8
|
+
import * as atlantisContext from "../AtlantisContext/AtlantisContext";
|
|
9
|
+
import { defaultValues as contextDefaultValue } from "../AtlantisContext";
|
|
10
|
+
|
|
11
|
+
const mockOnOpen = jest.fn();
|
|
12
|
+
|
|
13
|
+
function ButtonGroupForTest(props: ButtonGroupProps) {
|
|
14
|
+
return (
|
|
15
|
+
<Host>
|
|
16
|
+
<ButtonGroup
|
|
17
|
+
bottomSheetHeading={props.bottomSheetHeading}
|
|
18
|
+
showCancelInBottomSheet={props.showCancelInBottomSheet}
|
|
19
|
+
onOpenBottomSheet={mockOnOpen}
|
|
20
|
+
>
|
|
21
|
+
{props.children}
|
|
22
|
+
</ButtonGroup>
|
|
23
|
+
</Host>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
it("renders a single primary action", () => {
|
|
28
|
+
const createAction = jest.fn();
|
|
29
|
+
const { getByText, queryByLabelText } = render(
|
|
30
|
+
<ButtonGroupForTest>
|
|
31
|
+
<ButtonGroup.PrimaryAction
|
|
32
|
+
label="Create"
|
|
33
|
+
icon="plus"
|
|
34
|
+
onPress={createAction}
|
|
35
|
+
/>
|
|
36
|
+
</ButtonGroupForTest>,
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
expect(getByText("Create")).not.toBeNull();
|
|
40
|
+
expect(queryByLabelText(messages.more.defaultMessage)).toBeNull();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("renders 2 primary actions", () => {
|
|
44
|
+
const createAction = jest.fn();
|
|
45
|
+
const editAction = jest.fn();
|
|
46
|
+
const { getByText, queryByLabelText } = render(
|
|
47
|
+
<ButtonGroupForTest>
|
|
48
|
+
<ButtonGroup.PrimaryAction
|
|
49
|
+
label="Create"
|
|
50
|
+
icon="plus"
|
|
51
|
+
onPress={createAction}
|
|
52
|
+
/>
|
|
53
|
+
<ButtonGroup.PrimaryAction
|
|
54
|
+
label="Edit"
|
|
55
|
+
icon="edit"
|
|
56
|
+
onPress={editAction}
|
|
57
|
+
/>
|
|
58
|
+
</ButtonGroupForTest>,
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
expect(getByText("Create")).not.toBeNull();
|
|
62
|
+
expect(getByText("Edit")).not.toBeNull();
|
|
63
|
+
expect(queryByLabelText(messages.more.defaultMessage)).toBeNull();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("does not render more than 2 primary actions but adds a More button", () => {
|
|
67
|
+
const createAction = jest.fn();
|
|
68
|
+
const editAction = jest.fn();
|
|
69
|
+
const mysteryAction = jest.fn();
|
|
70
|
+
const { getByText, queryByText, getByLabelText } = render(
|
|
71
|
+
<ButtonGroupForTest>
|
|
72
|
+
<ButtonGroup.PrimaryAction
|
|
73
|
+
label="Create"
|
|
74
|
+
icon="plus"
|
|
75
|
+
onPress={createAction}
|
|
76
|
+
/>
|
|
77
|
+
<ButtonGroup.PrimaryAction
|
|
78
|
+
label="Edit"
|
|
79
|
+
icon="edit"
|
|
80
|
+
onPress={editAction}
|
|
81
|
+
/>
|
|
82
|
+
<ButtonGroup.PrimaryAction
|
|
83
|
+
label="Mystery"
|
|
84
|
+
icon="edit"
|
|
85
|
+
onPress={mysteryAction}
|
|
86
|
+
/>
|
|
87
|
+
</ButtonGroupForTest>,
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
expect(getByText("Create")).not.toBeNull();
|
|
91
|
+
expect(getByText("Edit")).not.toBeNull();
|
|
92
|
+
expect(queryByText("Mystery")).toBeNull();
|
|
93
|
+
expect(getByLabelText(messages.more.defaultMessage)).toBeDefined();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("does not render secondary actions", () => {
|
|
97
|
+
const createAction = jest.fn();
|
|
98
|
+
const editAction = jest.fn();
|
|
99
|
+
const deleteAction = jest.fn();
|
|
100
|
+
const { getByText, queryByText, getByLabelText } = render(
|
|
101
|
+
<ButtonGroupForTest>
|
|
102
|
+
<ButtonGroup.PrimaryAction
|
|
103
|
+
label="Create"
|
|
104
|
+
icon="plus"
|
|
105
|
+
onPress={createAction}
|
|
106
|
+
/>
|
|
107
|
+
<ButtonGroup.SecondaryAction
|
|
108
|
+
label="Edit"
|
|
109
|
+
icon="edit"
|
|
110
|
+
onPress={editAction}
|
|
111
|
+
/>
|
|
112
|
+
<ButtonGroup.SecondaryAction
|
|
113
|
+
label="Delete"
|
|
114
|
+
icon="trash"
|
|
115
|
+
onPress={deleteAction}
|
|
116
|
+
/>
|
|
117
|
+
</ButtonGroupForTest>,
|
|
118
|
+
);
|
|
119
|
+
expect(getByText("Create")).not.toBeNull();
|
|
120
|
+
expect(queryByText("Edit")).toBeNull();
|
|
121
|
+
expect(queryByText("Delete")).toBeNull();
|
|
122
|
+
expect(getByLabelText(messages.more.defaultMessage)).toBeDefined();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("renders first secondary action as a primary action button if no primary action specified", () => {
|
|
126
|
+
const editAction = jest.fn();
|
|
127
|
+
const deleteAction = jest.fn();
|
|
128
|
+
const { queryByText, getByText, getByLabelText } = render(
|
|
129
|
+
<ButtonGroupForTest>
|
|
130
|
+
<ButtonGroup.SecondaryAction
|
|
131
|
+
label="Edit"
|
|
132
|
+
icon="edit"
|
|
133
|
+
onPress={editAction}
|
|
134
|
+
/>
|
|
135
|
+
<ButtonGroup.SecondaryAction
|
|
136
|
+
label="Delete"
|
|
137
|
+
icon="trash"
|
|
138
|
+
onPress={deleteAction}
|
|
139
|
+
/>
|
|
140
|
+
</ButtonGroupForTest>,
|
|
141
|
+
);
|
|
142
|
+
expect(getByText("Edit")).not.toBeNull();
|
|
143
|
+
expect(queryByText("Delete")).toBeNull();
|
|
144
|
+
expect(getByLabelText(messages.more.defaultMessage)).toBeDefined();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("fires the press handlers when the primary action buttons are pressed", () => {
|
|
148
|
+
const createAction = jest.fn();
|
|
149
|
+
const editAction = jest.fn();
|
|
150
|
+
const { getByText } = render(
|
|
151
|
+
<ButtonGroupForTest>
|
|
152
|
+
<ButtonGroup.PrimaryAction
|
|
153
|
+
label="Create"
|
|
154
|
+
icon="plus"
|
|
155
|
+
onPress={createAction}
|
|
156
|
+
/>
|
|
157
|
+
<ButtonGroup.PrimaryAction
|
|
158
|
+
label="Edit"
|
|
159
|
+
icon="edit"
|
|
160
|
+
onPress={editAction}
|
|
161
|
+
/>
|
|
162
|
+
</ButtonGroupForTest>,
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
fireEvent.press(getByText("Create"));
|
|
166
|
+
expect(createAction).toHaveBeenCalled();
|
|
167
|
+
fireEvent.press(getByText("Edit"));
|
|
168
|
+
expect(editAction).toHaveBeenCalled();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("opens the secondary action menu when the More button is pressed", () => {
|
|
172
|
+
const createAction = jest.fn();
|
|
173
|
+
const editAction = jest.fn();
|
|
174
|
+
const deleteAction = jest.fn();
|
|
175
|
+
const { getByText, queryByText, getByLabelText } = render(
|
|
176
|
+
<ButtonGroupForTest>
|
|
177
|
+
<ButtonGroup.PrimaryAction
|
|
178
|
+
label="Create"
|
|
179
|
+
icon="plus"
|
|
180
|
+
onPress={createAction}
|
|
181
|
+
/>
|
|
182
|
+
<ButtonGroup.SecondaryAction
|
|
183
|
+
label="Edit"
|
|
184
|
+
icon="edit"
|
|
185
|
+
onPress={editAction}
|
|
186
|
+
/>
|
|
187
|
+
<ButtonGroup.SecondaryAction
|
|
188
|
+
label="Delete"
|
|
189
|
+
icon="trash"
|
|
190
|
+
onPress={deleteAction}
|
|
191
|
+
/>
|
|
192
|
+
</ButtonGroupForTest>,
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
expect(queryByText("Edit")).toBeNull();
|
|
196
|
+
|
|
197
|
+
fireEvent.press(getByLabelText(messages.more.defaultMessage));
|
|
198
|
+
|
|
199
|
+
expect(getByText("Edit")).not.toBeNull();
|
|
200
|
+
expect(getByText("Delete")).not.toBeNull();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("renders heading and cancel options if passed in", () => {
|
|
204
|
+
const createAction = jest.fn();
|
|
205
|
+
const editAction = jest.fn();
|
|
206
|
+
const { getByText, getByLabelText } = render(
|
|
207
|
+
<ButtonGroupForTest
|
|
208
|
+
bottomSheetHeading={"Heading"}
|
|
209
|
+
showCancelInBottomSheet={true}
|
|
210
|
+
>
|
|
211
|
+
<ButtonGroup.PrimaryAction
|
|
212
|
+
label="Create"
|
|
213
|
+
icon="plus"
|
|
214
|
+
onPress={createAction}
|
|
215
|
+
/>
|
|
216
|
+
<ButtonGroup.SecondaryAction
|
|
217
|
+
label="Edit"
|
|
218
|
+
icon="edit"
|
|
219
|
+
onPress={editAction}
|
|
220
|
+
/>
|
|
221
|
+
</ButtonGroupForTest>,
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
fireEvent.press(getByLabelText(messages.more.defaultMessage));
|
|
225
|
+
|
|
226
|
+
expect(getByText("Heading")).not.toBeNull();
|
|
227
|
+
expect(getByText("Cancel")).not.toBeNull();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("renders custom button for primary action if passed in", () => {
|
|
231
|
+
const createAction = jest.fn();
|
|
232
|
+
|
|
233
|
+
const customCreateButton = (
|
|
234
|
+
<Button label="CustomCreate" onPress={createAction} />
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
const { queryByText, getByText } = render(
|
|
238
|
+
<ButtonGroupForTest>
|
|
239
|
+
<ButtonGroup.PrimaryAction
|
|
240
|
+
label="Create"
|
|
241
|
+
icon="plus"
|
|
242
|
+
onPress={createAction}
|
|
243
|
+
customButton={customCreateButton}
|
|
244
|
+
/>
|
|
245
|
+
</ButtonGroupForTest>,
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
expect(getByText("CustomCreate")).not.toBeNull();
|
|
249
|
+
expect(queryByText("Create")).toBeNull();
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("calls onOpenBottomSheet when the secondary actions are opened", () => {
|
|
253
|
+
const createAction = jest.fn();
|
|
254
|
+
const editAction = jest.fn();
|
|
255
|
+
const deleteAction = jest.fn();
|
|
256
|
+
const { getByLabelText } = render(
|
|
257
|
+
<ButtonGroupForTest>
|
|
258
|
+
<ButtonGroup.PrimaryAction
|
|
259
|
+
label="Create"
|
|
260
|
+
icon="plus"
|
|
261
|
+
onPress={createAction}
|
|
262
|
+
/>
|
|
263
|
+
<ButtonGroup.SecondaryAction
|
|
264
|
+
label="Edit"
|
|
265
|
+
icon="edit"
|
|
266
|
+
onPress={editAction}
|
|
267
|
+
/>
|
|
268
|
+
<ButtonGroup.SecondaryAction
|
|
269
|
+
label="Delete"
|
|
270
|
+
icon="trash"
|
|
271
|
+
onPress={deleteAction}
|
|
272
|
+
/>
|
|
273
|
+
</ButtonGroupForTest>,
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
fireEvent.press(getByLabelText(messages.more.defaultMessage));
|
|
277
|
+
|
|
278
|
+
expect(mockOnOpen).toHaveBeenCalled();
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
describe("ButtonGroup Offline/Online", () => {
|
|
282
|
+
const atlantisContextSpy = jest.spyOn(atlantisContext, "useAtlantisContext");
|
|
283
|
+
|
|
284
|
+
afterEach(() => {
|
|
285
|
+
jest.restoreAllMocks();
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
const handlePress = jest.fn();
|
|
289
|
+
const label = "Click me";
|
|
290
|
+
const setup = () =>
|
|
291
|
+
render(
|
|
292
|
+
<ButtonGroupForTest>
|
|
293
|
+
<ButtonGroup.PrimaryAction
|
|
294
|
+
label={label}
|
|
295
|
+
icon="plus"
|
|
296
|
+
onPress={handlePress}
|
|
297
|
+
/>
|
|
298
|
+
</ButtonGroupForTest>,
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
it("should show an alert and not fire the onPress", () => {
|
|
302
|
+
const alertSpy = jest.spyOn(Alert, "alert");
|
|
303
|
+
atlantisContextSpy.mockReturnValue({
|
|
304
|
+
...contextDefaultValue,
|
|
305
|
+
isOnline: false,
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
const { getByText } = setup();
|
|
309
|
+
|
|
310
|
+
fireEvent.press(getByText(label));
|
|
311
|
+
|
|
312
|
+
expect(alertSpy).toHaveBeenCalled();
|
|
313
|
+
expect(handlePress).not.toHaveBeenCalled();
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it("should fire the onPress and not show an alert", () => {
|
|
317
|
+
const alertSpy = jest.spyOn(Alert, "alert");
|
|
318
|
+
const { getByText } = setup();
|
|
319
|
+
|
|
320
|
+
fireEvent.press(getByText(label));
|
|
321
|
+
|
|
322
|
+
expect(handlePress).toHaveBeenCalled();
|
|
323
|
+
expect(alertSpy).not.toHaveBeenCalled();
|
|
324
|
+
});
|
|
325
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import React, { useRef } from "react";
|
|
2
|
+
import { useIntl } from "react-intl";
|
|
3
|
+
import { View } from "react-native";
|
|
4
|
+
import { PrimaryAction, SecondaryAction } from "./ButtonGroupAction";
|
|
5
|
+
import { messages } from "./messages";
|
|
6
|
+
import { styles } from "./ButtonGroup.style";
|
|
7
|
+
import { SecondaryActionSheet } from "./components/SecondaryActionSheet";
|
|
8
|
+
import { getActions, usePreventTapWhenOffline } from "./utils";
|
|
9
|
+
import { ButtonGroupActionElement } from "./types";
|
|
10
|
+
import { Button } from "../Button";
|
|
11
|
+
import { BottomSheetRef } from "../BottomSheet/BottomSheet";
|
|
12
|
+
|
|
13
|
+
export interface ButtonGroupProps {
|
|
14
|
+
readonly children: ButtonGroupActionElement | ButtonGroupActionElement[];
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Display a cancel button in the secondary bottom sheet footer.
|
|
18
|
+
*/
|
|
19
|
+
readonly showCancelInBottomSheet?: boolean;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* An optional heading to display in the secondary bottom sheet header.
|
|
23
|
+
*/
|
|
24
|
+
readonly bottomSheetHeading?: string;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Callback that is called when the secondary actions bottom sheet is opened.
|
|
28
|
+
*/
|
|
29
|
+
readonly onOpenBottomSheet?: () => void;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Callback that is called when the secondary actions bottom sheet is closed.
|
|
33
|
+
*/
|
|
34
|
+
readonly onCloseBottomSheet?: () => void;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Allows you to Tap the button while offline
|
|
38
|
+
*/
|
|
39
|
+
readonly allowTapWhenOffline?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function ButtonGroup({
|
|
43
|
+
children,
|
|
44
|
+
showCancelInBottomSheet,
|
|
45
|
+
bottomSheetHeading,
|
|
46
|
+
onOpenBottomSheet,
|
|
47
|
+
onCloseBottomSheet,
|
|
48
|
+
allowTapWhenOffline = false,
|
|
49
|
+
}: ButtonGroupProps): JSX.Element {
|
|
50
|
+
const { formatMessage } = useIntl();
|
|
51
|
+
const { handlePress } = usePreventTapWhenOffline();
|
|
52
|
+
const secondaryActionsRef = useRef<BottomSheetRef>();
|
|
53
|
+
const { primaryActions, secondaryActions } = getActions(children);
|
|
54
|
+
return (
|
|
55
|
+
<View style={styles.buttonGroup}>
|
|
56
|
+
{primaryActions.map((action, index) => {
|
|
57
|
+
const {
|
|
58
|
+
label,
|
|
59
|
+
onPress,
|
|
60
|
+
buttonType,
|
|
61
|
+
buttonVariation,
|
|
62
|
+
icon,
|
|
63
|
+
customButton,
|
|
64
|
+
loading,
|
|
65
|
+
} = action.props;
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<View style={styles.button} key={index}>
|
|
69
|
+
{customButton || (
|
|
70
|
+
<Button
|
|
71
|
+
label={label}
|
|
72
|
+
accessibilityLabel={label}
|
|
73
|
+
onPress={allowTapWhenOffline ? onPress : handlePress(onPress)}
|
|
74
|
+
type={buttonType}
|
|
75
|
+
variation={buttonVariation}
|
|
76
|
+
fullHeight={true}
|
|
77
|
+
icon={icon}
|
|
78
|
+
loading={loading}
|
|
79
|
+
/>
|
|
80
|
+
)}
|
|
81
|
+
</View>
|
|
82
|
+
);
|
|
83
|
+
})}
|
|
84
|
+
|
|
85
|
+
{secondaryActions.length > 0 && (
|
|
86
|
+
<View style={styles.moreButton}>
|
|
87
|
+
<Button
|
|
88
|
+
icon={"more"}
|
|
89
|
+
accessibilityLabel={formatMessage(messages.more)}
|
|
90
|
+
onPress={handlePress(openBottomSheet)}
|
|
91
|
+
fullHeight={true}
|
|
92
|
+
/>
|
|
93
|
+
</View>
|
|
94
|
+
)}
|
|
95
|
+
|
|
96
|
+
<SecondaryActionSheet
|
|
97
|
+
heading={bottomSheetHeading}
|
|
98
|
+
showCancel={showCancelInBottomSheet}
|
|
99
|
+
secondaryActionsRef={secondaryActionsRef}
|
|
100
|
+
actions={secondaryActions.map(secondaryAction => secondaryAction.props)}
|
|
101
|
+
onOpenBottomSheet={onOpenBottomSheet}
|
|
102
|
+
onCloseBottomSheet={onCloseBottomSheet}
|
|
103
|
+
/>
|
|
104
|
+
</View>
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
function openBottomSheet() {
|
|
108
|
+
secondaryActionsRef.current?.open();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
ButtonGroup.PrimaryAction = PrimaryAction;
|
|
113
|
+
ButtonGroup.SecondaryAction = SecondaryAction;
|