@terreno/ui 0.13.3 → 0.14.1
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/ActionSheet.d.ts +5 -5
- package/dist/ActionSheet.js +2 -2
- package/dist/ActionSheet.js.map +1 -1
- package/dist/Avatar.js +1 -1
- package/dist/Avatar.js.map +1 -1
- package/dist/Banner.js.map +1 -1
- package/dist/Box.js +2 -0
- package/dist/Box.js.map +1 -1
- package/dist/Button.d.ts +2 -2
- package/dist/Button.js +35 -23
- package/dist/Button.js.map +1 -1
- package/dist/Common.d.ts +16 -4
- package/dist/Common.js +4 -4
- package/dist/Common.js.map +1 -1
- package/dist/ConsentFormScreen.js +3 -3
- package/dist/ConsentFormScreen.js.map +1 -1
- package/dist/ConsentNavigator.d.ts +1 -1
- package/dist/ConsentNavigator.js +2 -1
- package/dist/ConsentNavigator.js.map +1 -1
- package/dist/CustomSelectField.js +3 -1
- package/dist/CustomSelectField.js.map +1 -1
- package/dist/DataTable.js +1 -1
- package/dist/DataTable.js.map +1 -1
- package/dist/DateTimeActionSheet.js +2 -1
- package/dist/DateTimeActionSheet.js.map +1 -1
- package/dist/DateTimeField.js +3 -2
- package/dist/DateTimeField.js.map +1 -1
- package/dist/DateUtilities.d.ts +25 -25
- package/dist/DateUtilities.js +31 -32
- package/dist/DateUtilities.js.map +1 -1
- package/dist/HeightField.js.map +1 -1
- package/dist/Hyperlink.js +19 -9
- package/dist/Hyperlink.js.map +1 -1
- package/dist/IconButton.js.map +1 -1
- package/dist/ImageBackground.d.ts +2 -5
- package/dist/ImageBackground.js +1 -1
- package/dist/ImageBackground.js.map +1 -1
- package/dist/MediaQuery.d.ts +4 -4
- package/dist/MediaQuery.js +8 -8
- package/dist/MediaQuery.js.map +1 -1
- package/dist/ModalSheet.d.ts +3 -2
- package/dist/ModalSheet.js +1 -1
- package/dist/ModalSheet.js.map +1 -1
- package/dist/OfflineBanner.d.ts +21 -0
- package/dist/OfflineBanner.js +25 -0
- package/dist/OfflineBanner.js.map +1 -0
- package/dist/OpenAPIContext.js +1 -1
- package/dist/OpenAPIContext.js.map +1 -1
- package/dist/Page.d.ts +1 -0
- package/dist/Page.js +7 -2
- package/dist/Page.js.map +1 -1
- package/dist/Pagination.js.map +1 -1
- package/dist/Permissions.js +3 -0
- package/dist/Permissions.js.map +1 -1
- package/dist/PickerSelect.d.ts +1 -1
- package/dist/PickerSelect.js +9 -6
- package/dist/PickerSelect.js.map +1 -1
- package/dist/SelectField.js +1 -1
- package/dist/SelectField.js.map +1 -1
- package/dist/SplitPage.js +7 -2
- package/dist/SplitPage.js.map +1 -1
- package/dist/SplitPage.native.js +4 -1
- package/dist/SplitPage.native.js.map +1 -1
- package/dist/TapToEdit.d.ts +1 -1
- package/dist/TapToEdit.js +12 -14
- package/dist/TapToEdit.js.map +1 -1
- package/dist/Toast.js.map +1 -1
- package/dist/ToastNotifications.js +2 -2
- package/dist/ToastNotifications.js.map +1 -1
- package/dist/Tooltip.d.ts +24 -1
- package/dist/Tooltip.js +2 -2
- package/dist/Tooltip.js.map +1 -1
- package/dist/Unifier.d.ts +3 -3
- package/dist/Unifier.js +15 -12
- package/dist/Unifier.js.map +1 -1
- package/dist/Utilities.d.ts +12 -8
- package/dist/Utilities.js +13 -15
- package/dist/Utilities.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/signUp/PasswordRequirements.js +3 -3
- package/dist/signUp/PasswordRequirements.js.map +1 -1
- package/dist/table/TableHeaderCell.js +1 -9
- package/dist/table/TableHeaderCell.js.map +1 -1
- package/dist/table/tableContext.d.ts +1 -1
- package/dist/table/tableContext.js +2 -2
- package/dist/table/tableContext.js.map +1 -1
- package/package.json +2 -1
- package/src/ActionSheet.test.tsx +1 -0
- package/src/ActionSheet.tsx +8 -6
- package/src/Avatar.tsx +9 -2
- package/src/Badge.test.tsx +1 -0
- package/src/Banner.test.tsx +71 -0
- package/src/Banner.tsx +1 -1
- package/src/Box.test.tsx +1 -0
- package/src/Box.tsx +10 -6
- package/src/Button.test.tsx +35 -0
- package/src/Button.tsx +65 -34
- package/src/Common.ts +42 -19
- package/src/ConsentFormScreen.test.tsx +124 -0
- package/src/ConsentFormScreen.tsx +18 -6
- package/src/ConsentNavigator.test.tsx +1 -0
- package/src/ConsentNavigator.tsx +5 -3
- package/src/CustomSelectField.tsx +3 -1
- package/src/DataTable.test.tsx +218 -0
- package/src/DataTable.tsx +1 -1
- package/src/DateTimeActionSheet.tsx +7 -3
- package/src/DateTimeField.test.tsx +1 -0
- package/src/DateTimeField.tsx +3 -2
- package/src/DateUtilities.test.ts +111 -0
- package/src/DateUtilities.tsx +43 -44
- package/src/DecimalRangeActionSheet.test.tsx +28 -0
- package/src/ErrorBoundary.test.tsx +1 -0
- package/src/HeightActionSheet.test.tsx +16 -0
- package/src/HeightField.test.tsx +106 -1
- package/src/HeightField.tsx +2 -1
- package/src/Hyperlink.tsx +83 -52
- package/src/IconButton.tsx +1 -1
- package/src/ImageBackground.tsx +5 -6
- package/src/MediaQuery.ts +8 -8
- package/src/MobileAddressAutoComplete.test.tsx +20 -1
- package/src/ModalSheet.test.tsx +1 -5
- package/src/ModalSheet.tsx +15 -6
- package/src/NumberField.test.tsx +14 -0
- package/src/OfflineBanner.test.tsx +70 -0
- package/src/OfflineBanner.tsx +54 -0
- package/src/OpenAPIContext.tsx +3 -2
- package/src/Page.test.tsx +28 -0
- package/src/Page.tsx +18 -2
- package/src/Pagination.tsx +1 -1
- package/src/Permissions.ts +3 -0
- package/src/PickerSelect.tsx +20 -17
- package/src/SelectBadge.test.tsx +1 -0
- package/src/SelectField.tsx +1 -1
- package/src/Signature.test.tsx +1 -0
- package/src/SplitPage.native.tsx +2 -0
- package/src/SplitPage.tsx +6 -1
- package/src/TapToEdit.test.tsx +48 -0
- package/src/TapToEdit.tsx +13 -14
- package/src/Toast.tsx +1 -1
- package/src/ToastNotifications.test.tsx +738 -0
- package/src/ToastNotifications.tsx +3 -6
- package/src/Tooltip.test.tsx +586 -8
- package/src/Tooltip.tsx +2 -2
- package/src/Unifier.ts +20 -16
- package/src/Utilities.tsx +20 -19
- package/src/WebAddressAutocomplete.test.tsx +138 -0
- package/src/WebDropdownMenu.test.tsx +23 -0
- package/src/__snapshots__/AddressField.test.tsx.snap +3 -1
- package/src/__snapshots__/Button.test.tsx.snap +92 -50
- package/src/__snapshots__/CustomSelectField.test.tsx.snap +21 -7
- package/src/__snapshots__/DecimalRangeActionSheet.test.tsx.snap +14 -8
- package/src/__snapshots__/ErrorPage.test.tsx.snap +7 -4
- package/src/__snapshots__/Field.test.tsx.snap +18 -6
- package/src/__snapshots__/HeightActionSheet.test.tsx.snap +14 -8
- package/src/__snapshots__/HeightField.test.tsx.snap +35 -20
- package/src/__snapshots__/InfoModalIcon.test.tsx.snap +28 -16
- package/src/__snapshots__/Modal.test.tsx.snap +19 -10
- package/src/__snapshots__/ModalSheet.test.tsx.snap +0 -1
- package/src/__snapshots__/NumberPickerActionSheet.test.tsx.snap +14 -8
- package/src/__snapshots__/Page.test.tsx.snap +7 -4
- package/src/__snapshots__/SelectField.test.tsx.snap +18 -6
- package/src/__snapshots__/TerrenoProvider.test.tsx.snap +0 -2
- package/src/__snapshots__/TimezonePicker.test.tsx.snap +18 -6
- package/src/bunSetup.ts +25 -2
- package/src/index.tsx +2 -1
- package/src/login/LoginScreen.test.tsx +23 -1
- package/src/login/__snapshots__/LoginScreen.test.tsx.snap +15 -6
- package/src/signUp/PasswordRequirements.tsx +9 -6
- package/src/signUp/__snapshots__/PasswordRequirements.test.tsx.snap +50 -2
- package/src/signUp/__snapshots__/SignUpScreen.test.tsx.snap +35 -5
- package/src/table/TableHeaderCell.tsx +8 -11
- package/src/table/TableRow.test.tsx +31 -1
- package/src/table/__snapshots__/TableBadge.test.tsx.snap +3 -1
- package/src/table/__snapshots__/TableHeaderCell.test.tsx.snap +2 -0
- package/src/table/tableContext.tsx +2 -2
- package/src/types/react-native-swiper-flatlist.d.ts +1 -0
- package/src/useStoredState.test.tsx +47 -0
package/src/ActionSheet.tsx
CHANGED
|
@@ -56,7 +56,7 @@ export const styles = StyleSheet.create({
|
|
|
56
56
|
},
|
|
57
57
|
});
|
|
58
58
|
|
|
59
|
-
export
|
|
59
|
+
export const getDeviceHeight = (statusBarTranslucent: boolean | undefined): number => {
|
|
60
60
|
const height = Dimensions.get("window").height;
|
|
61
61
|
|
|
62
62
|
if (Platform.OS === "android" && !statusBarTranslucent) {
|
|
@@ -64,7 +64,7 @@ export function getDeviceHeight(statusBarTranslucent: boolean | undefined): numb
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
return height;
|
|
67
|
-
}
|
|
67
|
+
};
|
|
68
68
|
|
|
69
69
|
export const getElevation = (elevation?: number) => {
|
|
70
70
|
if (!elevation) {
|
|
@@ -133,7 +133,7 @@ const defaultProps = {
|
|
|
133
133
|
|
|
134
134
|
type Props = Partial<typeof defaultProps> & ActionSheetProps;
|
|
135
135
|
|
|
136
|
-
export class ActionSheet extends Component<Props, State,
|
|
136
|
+
export class ActionSheet extends Component<Props, State, unknown> {
|
|
137
137
|
static defaultProps = defaultProps;
|
|
138
138
|
|
|
139
139
|
actionSheetHeight = 0;
|
|
@@ -144,7 +144,7 @@ export class ActionSheet extends Component<Props, State, any> {
|
|
|
144
144
|
|
|
145
145
|
prevScroll = 0;
|
|
146
146
|
|
|
147
|
-
timeout:
|
|
147
|
+
timeout: ReturnType<typeof setTimeout> | null = null;
|
|
148
148
|
|
|
149
149
|
offsetY = 0;
|
|
150
150
|
|
|
@@ -164,8 +164,10 @@ export class ActionSheet extends Component<Props, State, any> {
|
|
|
164
164
|
|
|
165
165
|
deviceLayoutCalled = false;
|
|
166
166
|
|
|
167
|
+
// biome-ignore lint/suspicious/noExplicitAny: FlatList ref is accessed via internal _listRef._scrollRef which is not part of the public type
|
|
167
168
|
scrollViewRef: React.RefObject<any>;
|
|
168
169
|
|
|
170
|
+
// biome-ignore lint/suspicious/noExplicitAny: SafeAreaView ref is passed to findNodeHandle and accessed via untyped React Native internals
|
|
169
171
|
safeAreaViewRef: React.RefObject<any>;
|
|
170
172
|
|
|
171
173
|
transformValue: Animated.Value;
|
|
@@ -669,7 +671,7 @@ export class ActionSheet extends Component<Props, State, any> {
|
|
|
669
671
|
this.keyboardDidHideListener?.remove();
|
|
670
672
|
}
|
|
671
673
|
|
|
672
|
-
_onDeviceLayout = async (_event:
|
|
674
|
+
_onDeviceLayout = async (_event: LayoutChangeEvent) => {
|
|
673
675
|
const event = {..._event};
|
|
674
676
|
|
|
675
677
|
if (this.timeout) {
|
|
@@ -721,7 +723,7 @@ export class ActionSheet extends Component<Props, State, any> {
|
|
|
721
723
|
return scrollPosition;
|
|
722
724
|
}
|
|
723
725
|
|
|
724
|
-
_keyExtractor = (item:
|
|
726
|
+
_keyExtractor = (item: string) => item;
|
|
725
727
|
|
|
726
728
|
render() {
|
|
727
729
|
const {scrollable, modalVisible, keyboard} = this.state;
|
package/src/Avatar.tsx
CHANGED
|
@@ -3,7 +3,14 @@ import {launchImageLibraryAsync} from "expo-image-picker";
|
|
|
3
3
|
import {LinearGradient} from "expo-linear-gradient";
|
|
4
4
|
import type React from "react";
|
|
5
5
|
import {type FC, useState} from "react";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
Image,
|
|
8
|
+
type ImageErrorEventData,
|
|
9
|
+
type NativeSyntheticEvent,
|
|
10
|
+
Pressable,
|
|
11
|
+
Text,
|
|
12
|
+
View,
|
|
13
|
+
} from "react-native";
|
|
7
14
|
|
|
8
15
|
import type {AvatarProps, CustomSvgProps} from "./Common";
|
|
9
16
|
import {Icon} from "./Icon";
|
|
@@ -88,7 +95,7 @@ export const Avatar: FC<AvatarProps> = ({
|
|
|
88
95
|
console.warn("Avatars with the status of 'imagePicker' should also have an onChange property.");
|
|
89
96
|
}
|
|
90
97
|
|
|
91
|
-
const handleImageError = (event:
|
|
98
|
+
const handleImageError = (event: NativeSyntheticEvent<ImageErrorEventData>) => {
|
|
92
99
|
setIsImageLoaded(false);
|
|
93
100
|
console.warn("Image load error: ", event);
|
|
94
101
|
};
|
package/src/Badge.test.tsx
CHANGED
package/src/Banner.test.tsx
CHANGED
|
@@ -168,4 +168,75 @@ describe("Banner", () => {
|
|
|
168
168
|
expect(setItemMock).not.toHaveBeenCalled();
|
|
169
169
|
});
|
|
170
170
|
});
|
|
171
|
+
|
|
172
|
+
it("hides banner when storage already has the dismissed flag", async () => {
|
|
173
|
+
const getItemMock = Unifier.storage.getItem as ReturnType<typeof mock>;
|
|
174
|
+
getItemMock.mockReturnValueOnce(Promise.resolve("true"));
|
|
175
|
+
|
|
176
|
+
const {queryByText} = renderWithTheme(
|
|
177
|
+
<Banner dismissible id="stored-banner" text="Previously dismissed" />
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
await waitFor(() => {
|
|
181
|
+
expect(queryByText("Previously dismissed")).toBeNull();
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("exercises the async .then path in useEffect", async () => {
|
|
186
|
+
const getItemMock = Unifier.storage.getItem as ReturnType<typeof mock>;
|
|
187
|
+
getItemMock.mockReturnValueOnce(Promise.resolve(null));
|
|
188
|
+
|
|
189
|
+
const {queryByText} = renderWithTheme(
|
|
190
|
+
<Banner dismissible id="flush-banner" text="Flush banner" />
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
await act(async () => {
|
|
194
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
expect(queryByText("Flush banner")).toBeTruthy();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("renders button without icon name (text-only button path)", async () => {
|
|
201
|
+
const handleClick = mock(() => Promise.resolve());
|
|
202
|
+
const {getByText} = renderWithTheme(
|
|
203
|
+
<Banner
|
|
204
|
+
buttonOnClick={handleClick}
|
|
205
|
+
buttonText="TextOnly"
|
|
206
|
+
id="textonly-banner"
|
|
207
|
+
text="Banner"
|
|
208
|
+
/>
|
|
209
|
+
);
|
|
210
|
+
expect(getByText("TextOnly")).toBeTruthy();
|
|
211
|
+
|
|
212
|
+
await act(async () => {
|
|
213
|
+
fireEvent.press(getByText("TextOnly"));
|
|
214
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
await waitFor(() => {
|
|
218
|
+
expect(handleClick).toHaveBeenCalled();
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("covers catch block when buttonOnClick rejects", async () => {
|
|
223
|
+
const handleClick = mock(() => Promise.reject(new Error("boom")));
|
|
224
|
+
const {UNSAFE_root} = renderWithTheme(
|
|
225
|
+
<Banner buttonOnClick={handleClick} buttonText="Fail" id="catch-banner" text="Banner" />
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
const pressable = UNSAFE_root.findAll(
|
|
229
|
+
(node) => node.props?.["aria-label"] === "Fail" && typeof node.props?.onPress === "function"
|
|
230
|
+
)[0];
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
await act(async () => {
|
|
234
|
+
await pressable.props.onPress();
|
|
235
|
+
});
|
|
236
|
+
} catch (_e) {
|
|
237
|
+
// Expected: catch block in BannerButton re-throws
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
expect(handleClick).toHaveBeenCalled();
|
|
241
|
+
});
|
|
171
242
|
});
|
package/src/Banner.tsx
CHANGED
|
@@ -54,7 +54,7 @@ const BannerButton = ({
|
|
|
54
54
|
alignItems: "center",
|
|
55
55
|
alignSelf: "stretch",
|
|
56
56
|
backgroundColor: theme.surface.base,
|
|
57
|
-
borderRadius: theme.radius.rounded
|
|
57
|
+
borderRadius: theme.radius.rounded,
|
|
58
58
|
flexDirection: "column",
|
|
59
59
|
justifyContent: "center",
|
|
60
60
|
paddingHorizontal: 12,
|
package/src/Box.test.tsx
CHANGED
package/src/Box.tsx
CHANGED
|
@@ -86,7 +86,9 @@ export const Box = React.forwardRef((props: BoxProps, ref) => {
|
|
|
86
86
|
|
|
87
87
|
const BOX_STYLE_MAP: {
|
|
88
88
|
[prop: string]: (
|
|
89
|
+
// biome-ignore lint/suspicious/noExplicitAny: Box's style mapper accepts heterogeneous prop values (spacing, colors, dimensions, theme tokens, booleans) that cannot be enumerated in a single union; the mapper functions narrow at call site
|
|
89
90
|
value: any,
|
|
91
|
+
// biome-ignore lint/suspicious/noExplicitAny: see above - heterogeneous prop values
|
|
90
92
|
all: {[prop: string]: any}
|
|
91
93
|
) => {[style: string]: string | number} | {};
|
|
92
94
|
} = {
|
|
@@ -125,8 +127,8 @@ export const Box = React.forwardRef((props: BoxProps, ref) => {
|
|
|
125
127
|
},
|
|
126
128
|
bottom: (bottom) => ({bottom: bottom ? 0 : undefined}),
|
|
127
129
|
color: (value: keyof SurfaceTheme) => ({backgroundColor: theme.surface[value]}),
|
|
128
|
-
direction: (value:
|
|
129
|
-
display: (value:
|
|
130
|
+
direction: (value: "row" | "column") => ({display: "flex", flexDirection: value}),
|
|
131
|
+
display: (value: "none" | "flex" | "block" | "inlineBlock" | "visuallyHidden") => {
|
|
130
132
|
if (value === "none") {
|
|
131
133
|
return {display: "none"};
|
|
132
134
|
}
|
|
@@ -157,7 +159,7 @@ export const Box = React.forwardRef((props: BoxProps, ref) => {
|
|
|
157
159
|
},
|
|
158
160
|
justifyContent: (value: JustifyContent) => ({justifyContent: ALIGN_CONTENT[value]}),
|
|
159
161
|
left: (left) => ({left: left ? 0 : undefined}),
|
|
160
|
-
lgDirection: (value:
|
|
162
|
+
lgDirection: (value: "row" | "column") =>
|
|
161
163
|
mediaQueryLargerThan("lg") ? {display: "flex", flexDirection: value} : {},
|
|
162
164
|
margin: (value) => ({margin: getSpacing(value)}),
|
|
163
165
|
marginBottom: (value) => ({marginBottom: getSpacing(value)}),
|
|
@@ -182,7 +184,7 @@ export const Box = React.forwardRef((props: BoxProps, ref) => {
|
|
|
182
184
|
}
|
|
183
185
|
return {maxWidth: value};
|
|
184
186
|
},
|
|
185
|
-
mdDirection: (value:
|
|
187
|
+
mdDirection: (value: "row" | "column") =>
|
|
186
188
|
mediaQueryLargerThan("md") ? {display: "flex", flexDirection: value} : {},
|
|
187
189
|
minHeight: (value) => {
|
|
188
190
|
if (!isValidWidthHeight(value)) {
|
|
@@ -240,7 +242,7 @@ export const Box = React.forwardRef((props: BoxProps, ref) => {
|
|
|
240
242
|
return {elevation: 4};
|
|
241
243
|
}
|
|
242
244
|
},
|
|
243
|
-
smDirection: (value:
|
|
245
|
+
smDirection: (value: "row" | "column") =>
|
|
244
246
|
mediaQueryLargerThan("sm") ? {display: "flex", flexDirection: value} : {},
|
|
245
247
|
top: (top) => ({top: top ? 0 : undefined}),
|
|
246
248
|
width: (value) => {
|
|
@@ -262,7 +264,9 @@ export const Box = React.forwardRef((props: BoxProps, ref) => {
|
|
|
262
264
|
|
|
263
265
|
const scrollRef = props.scrollRef ?? React.createRef();
|
|
264
266
|
|
|
267
|
+
// biome-ignore lint/suspicious/noExplicitAny: the style object is assembled from heterogeneous mapper outputs and consumed by RN ViewStyle which has narrow union types per property
|
|
265
268
|
const propsToStyle = (): any => {
|
|
269
|
+
// biome-ignore lint/suspicious/noExplicitAny: same as above - heterogeneous style assembly
|
|
266
270
|
let style: any = {};
|
|
267
271
|
for (const prop of Object.keys(props) as Array<keyof typeof props>) {
|
|
268
272
|
const value = props[prop];
|
|
@@ -294,7 +298,7 @@ export const Box = React.forwardRef((props: BoxProps, ref) => {
|
|
|
294
298
|
await props.onHoverEnd?.();
|
|
295
299
|
};
|
|
296
300
|
|
|
297
|
-
let box;
|
|
301
|
+
let box: React.ReactElement;
|
|
298
302
|
|
|
299
303
|
// Adding the accessibilityRole of button throws a warning in React Native since we nest buttons
|
|
300
304
|
// within Box and RN does not support nested buttons
|
package/src/Button.test.tsx
CHANGED
|
@@ -56,6 +56,28 @@ describe("Button", () => {
|
|
|
56
56
|
expect(toJSON()).toMatchSnapshot();
|
|
57
57
|
});
|
|
58
58
|
|
|
59
|
+
it("defaults to scale press animation", () => {
|
|
60
|
+
const tree = renderWithTheme(<Button onClick={() => {}} text="Default animation" />).toJSON();
|
|
61
|
+
expect(Array.isArray(tree)).toBe(false);
|
|
62
|
+
expect(tree?.type).toBe("PressableScale");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("renders opacity press animation", () => {
|
|
66
|
+
const tree = renderWithTheme(
|
|
67
|
+
<Button onClick={() => {}} pressAnimation="opacity" text="Opacity" />
|
|
68
|
+
).toJSON();
|
|
69
|
+
expect(Array.isArray(tree)).toBe(false);
|
|
70
|
+
expect(tree?.type).toBe("PressableOpacity");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("renders no press animation", () => {
|
|
74
|
+
const tree = renderWithTheme(
|
|
75
|
+
<Button onClick={() => {}} pressAnimation="none" text="No animation" />
|
|
76
|
+
).toJSON();
|
|
77
|
+
expect(Array.isArray(tree)).toBe(false);
|
|
78
|
+
expect(tree?.type).toBe("PressableWithoutFeedback");
|
|
79
|
+
});
|
|
80
|
+
|
|
59
81
|
// Disabled state
|
|
60
82
|
it("renders disabled state", () => {
|
|
61
83
|
const {toJSON} = renderWithTheme(<Button disabled onClick={() => {}} text="Disabled" />);
|
|
@@ -111,6 +133,19 @@ describe("Button", () => {
|
|
|
111
133
|
});
|
|
112
134
|
});
|
|
113
135
|
|
|
136
|
+
it("does not call onClick again on the trailing debounce edge after rapid presses", async () => {
|
|
137
|
+
const handleClick = mock(() => Promise.resolve());
|
|
138
|
+
const {getByText} = renderWithTheme(<Button onClick={handleClick} text="Click" />);
|
|
139
|
+
|
|
140
|
+
await act(async () => {
|
|
141
|
+
fireEvent.press(getByText("Click"));
|
|
142
|
+
fireEvent.press(getByText("Click"));
|
|
143
|
+
await new Promise((resolve) => setTimeout(resolve, 700));
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
expect(handleClick).toHaveBeenCalledTimes(1);
|
|
147
|
+
});
|
|
148
|
+
|
|
114
149
|
// Confirmation modal tests
|
|
115
150
|
it("renders with confirmation modal props", () => {
|
|
116
151
|
const {toJSON} = renderWithTheme(
|
package/src/Button.tsx
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import FontAwesome6 from "@expo/vector-icons/FontAwesome6";
|
|
2
2
|
import debounce from "lodash/debounce";
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import {
|
|
4
|
+
type CustomPressableProps,
|
|
5
|
+
PressableOpacity,
|
|
6
|
+
PressableScale,
|
|
7
|
+
PressableWithoutFeedback,
|
|
8
|
+
} from "pressto";
|
|
9
|
+
import type React from "react";
|
|
10
|
+
import {lazy, Suspense, useCallback, useMemo, useState} from "react";
|
|
11
|
+
import {ActivityIndicator, Pressable, type PressableProps, Text, View} from "react-native";
|
|
5
12
|
|
|
6
13
|
import {Box} from "./Box";
|
|
7
|
-
import type {ButtonProps} from "./Common";
|
|
14
|
+
import type {ButtonPressAnimation, ButtonProps} from "./Common";
|
|
8
15
|
import {isMobileDevice} from "./MediaQuery";
|
|
9
16
|
import {useTheme} from "./Theme";
|
|
10
17
|
import {Tooltip} from "./Tooltip";
|
|
@@ -14,7 +21,20 @@ import {isNative} from "./Utilities";
|
|
|
14
21
|
// Lazy load Modal to break the circular dependency: Modal -> Button -> Modal
|
|
15
22
|
const LazyModal = lazy(() => import("./Modal").then((module) => ({default: module.Modal})));
|
|
16
23
|
|
|
17
|
-
const
|
|
24
|
+
const DEFAULT_BUTTON_PRESS_ANIMATION: ButtonPressAnimation = "scale";
|
|
25
|
+
|
|
26
|
+
const PRESSABLE_BY_ANIMATION: Record<
|
|
27
|
+
ButtonPressAnimation,
|
|
28
|
+
React.ComponentType<CustomPressableProps>
|
|
29
|
+
> = {
|
|
30
|
+
none: PressableWithoutFeedback,
|
|
31
|
+
opacity: PressableOpacity,
|
|
32
|
+
scale: PressableScale,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type ButtonPressableProps = CustomPressableProps & PressableProps;
|
|
36
|
+
|
|
37
|
+
const ButtonComponent: React.FC<ButtonProps> = ({
|
|
18
38
|
confirmationText = "Are you sure you want to continue?",
|
|
19
39
|
disabled = false,
|
|
20
40
|
fullWidth = false,
|
|
@@ -23,6 +43,7 @@ const ButtonComponent: FC<ButtonProps> = ({
|
|
|
23
43
|
loading: propsLoading,
|
|
24
44
|
modalTitle = "Confirm",
|
|
25
45
|
modalSubTitle,
|
|
46
|
+
pressAnimation = DEFAULT_BUTTON_PRESS_ANIMATION,
|
|
26
47
|
testID,
|
|
27
48
|
text,
|
|
28
49
|
variant = "primary",
|
|
@@ -66,41 +87,51 @@ const ButtonComponent: FC<ButtonProps> = ({
|
|
|
66
87
|
};
|
|
67
88
|
}, [disabled, variant, theme]);
|
|
68
89
|
|
|
90
|
+
const handlePress = useCallback(async (): Promise<void> => {
|
|
91
|
+
await Unifier.utils.haptic();
|
|
92
|
+
setLoading(true);
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
// If a confirmation is required, and the confirmation modal is not currently open,
|
|
96
|
+
// open it.
|
|
97
|
+
if (withConfirmation && !showConfirmation) {
|
|
98
|
+
setShowConfirmation(true);
|
|
99
|
+
} else if (!withConfirmation && onClick) {
|
|
100
|
+
// If a confirmation is not required, perform the action.
|
|
101
|
+
await onClick();
|
|
102
|
+
}
|
|
103
|
+
} catch (error) {
|
|
104
|
+
setLoading(false);
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
setLoading(false);
|
|
108
|
+
}, [onClick, showConfirmation, withConfirmation]);
|
|
109
|
+
|
|
110
|
+
const debouncedHandlePress = useMemo(
|
|
111
|
+
() => debounce(handlePress, 500, {leading: true, trailing: false}),
|
|
112
|
+
[handlePress]
|
|
113
|
+
);
|
|
114
|
+
|
|
69
115
|
if (!theme) {
|
|
70
116
|
return null;
|
|
71
117
|
}
|
|
72
118
|
|
|
119
|
+
const isPressDisabled = disabled || Boolean(loading);
|
|
120
|
+
const PressableComponent = (
|
|
121
|
+
isPressDisabled ? Pressable : PRESSABLE_BY_ANIMATION[pressAnimation]
|
|
122
|
+
) as React.ComponentType<ButtonPressableProps>;
|
|
123
|
+
const pressableInteractionProps = isPressDisabled ? {disabled: true} : {enabled: true};
|
|
124
|
+
|
|
73
125
|
return (
|
|
74
|
-
<
|
|
126
|
+
<PressableComponent
|
|
75
127
|
accessibilityHint={
|
|
76
128
|
withConfirmation ? "Opens a confirmation dialog" : "Press to perform action"
|
|
77
129
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
await Unifier.utils.haptic();
|
|
84
|
-
setLoading(true);
|
|
85
|
-
|
|
86
|
-
try {
|
|
87
|
-
// If a confirmation is required, and the confirmation modal is not currently open,
|
|
88
|
-
// open it
|
|
89
|
-
if (withConfirmation && !showConfirmation) {
|
|
90
|
-
setShowConfirmation(true);
|
|
91
|
-
} else if (!withConfirmation && onClick) {
|
|
92
|
-
// If a confirmation is not required, perform the action.
|
|
93
|
-
await onClick();
|
|
94
|
-
}
|
|
95
|
-
} catch (error) {
|
|
96
|
-
setLoading(false);
|
|
97
|
-
throw error;
|
|
98
|
-
}
|
|
99
|
-
setLoading(false);
|
|
100
|
-
},
|
|
101
|
-
500,
|
|
102
|
-
{leading: true}
|
|
103
|
-
)}
|
|
130
|
+
accessibilityLabel={text}
|
|
131
|
+
accessibilityRole="button"
|
|
132
|
+
accessibilityState={{disabled: isPressDisabled}}
|
|
133
|
+
{...pressableInteractionProps}
|
|
134
|
+
onPress={debouncedHandlePress}
|
|
104
135
|
style={{
|
|
105
136
|
alignItems: "center",
|
|
106
137
|
alignSelf: fullWidth ? "stretch" : "flex-start",
|
|
@@ -141,7 +172,7 @@ const ButtonComponent: FC<ButtonProps> = ({
|
|
|
141
172
|
<Suspense fallback={null}>
|
|
142
173
|
<LazyModal
|
|
143
174
|
onDismiss={() => setShowConfirmation(false)}
|
|
144
|
-
primaryButtonOnClick={async () => {
|
|
175
|
+
primaryButtonOnClick={async (): Promise<void> => {
|
|
145
176
|
await onClick();
|
|
146
177
|
setShowConfirmation(false);
|
|
147
178
|
}}
|
|
@@ -155,11 +186,11 @@ const ButtonComponent: FC<ButtonProps> = ({
|
|
|
155
186
|
/>
|
|
156
187
|
</Suspense>
|
|
157
188
|
)}
|
|
158
|
-
</
|
|
189
|
+
</PressableComponent>
|
|
159
190
|
);
|
|
160
191
|
};
|
|
161
192
|
|
|
162
|
-
export const Button: FC<ButtonProps> = (props) => {
|
|
193
|
+
export const Button: React.FC<ButtonProps> = (props) => {
|
|
163
194
|
const {tooltipText, tooltipIdealPosition, tooltipIncludeArrow = false} = props;
|
|
164
195
|
const isMobileOrNative = isMobileDevice() || isNative();
|
|
165
196
|
|