@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
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type React from "react";
|
|
2
|
+
|
|
3
|
+
import {Banner} from "./Banner";
|
|
4
|
+
import {Box} from "./Box";
|
|
5
|
+
|
|
6
|
+
export interface OfflineBannerProps {
|
|
7
|
+
/** Whether the server is currently reachable */
|
|
8
|
+
isOnline: boolean;
|
|
9
|
+
/** Number of mutations waiting to be synced */
|
|
10
|
+
queueLength: number;
|
|
11
|
+
/** Whether mutations are currently being replayed */
|
|
12
|
+
isSyncing: boolean;
|
|
13
|
+
/** testID for the root element */
|
|
14
|
+
testID?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Displays offline/syncing status banners. Renders nothing when online and idle.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```typescript
|
|
22
|
+
* const {isOnline, queueLength, isSyncing} = useServerStatus();
|
|
23
|
+
* <OfflineBanner isOnline={isOnline} queueLength={queueLength} isSyncing={isSyncing} />
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export const OfflineBanner: React.FC<OfflineBannerProps> = ({
|
|
27
|
+
isOnline,
|
|
28
|
+
queueLength,
|
|
29
|
+
isSyncing,
|
|
30
|
+
testID = "offline-banner",
|
|
31
|
+
}) => {
|
|
32
|
+
if (isSyncing) {
|
|
33
|
+
return (
|
|
34
|
+
<Box marginBottom={4} testID={testID}>
|
|
35
|
+
<Banner id="syncing-status" status="info" text="Syncing offline changes..." />
|
|
36
|
+
</Box>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!isOnline) {
|
|
41
|
+
const suffix =
|
|
42
|
+
queueLength > 0
|
|
43
|
+
? ` ${queueLength} pending change${queueLength !== 1 ? "s" : ""} will sync when you reconnect.`
|
|
44
|
+
: "";
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<Box marginBottom={4} testID={testID}>
|
|
48
|
+
<Banner id="offline-status" status="warning" text={`You're offline.${suffix}`} />
|
|
49
|
+
</Box>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return null;
|
|
54
|
+
};
|
package/src/OpenAPIContext.tsx
CHANGED
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
OpenAPIContextType,
|
|
9
9
|
OpenAPIProviderProps,
|
|
10
10
|
OpenAPISpec,
|
|
11
|
+
OpenApiProperty,
|
|
11
12
|
} from "./Common";
|
|
12
13
|
|
|
13
14
|
const OpenAPIContext = createContext<OpenAPIContextType | null>(null);
|
|
@@ -39,7 +40,7 @@ export const OpenAPIProvider = ({children, specUrl}: OpenAPIProviderProps): Reac
|
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
for (const dotField of dotFields.slice(1)) {
|
|
42
|
-
field = (field?.properties as
|
|
43
|
+
field = (field?.properties as Record<string, OpenApiProperty> | undefined)?.[dotField];
|
|
43
44
|
}
|
|
44
45
|
return field;
|
|
45
46
|
};
|
|
@@ -55,7 +56,7 @@ export const OpenAPIProvider = ({children, specUrl}: OpenAPIProviderProps): Reac
|
|
|
55
56
|
const data = (await response.json()) as OpenAPISpec;
|
|
56
57
|
setSpec(data);
|
|
57
58
|
})
|
|
58
|
-
.catch((error:
|
|
59
|
+
.catch((error: unknown) => console.error(`Error fetching OpenAPI spec: ${String(error)}`));
|
|
59
60
|
}, [specUrl]);
|
|
60
61
|
|
|
61
62
|
return (
|
package/src/Page.test.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import {afterAll, describe, expect, it, mock} from "bun:test";
|
|
|
2
2
|
import {act, fireEvent, waitFor} from "@testing-library/react-native";
|
|
3
3
|
import React, {type ReactNode} from "react";
|
|
4
4
|
import {Pressable, Text as RNText} from "react-native";
|
|
5
|
+
import {SafeAreaView} from "react-native-safe-area-context";
|
|
5
6
|
|
|
6
7
|
// Override the IconButton mock so the inline onClick arrows fire when pressed.
|
|
7
8
|
mock.module("./IconButton", () => ({
|
|
@@ -273,6 +274,33 @@ describe("Page", () => {
|
|
|
273
274
|
expect(routerBack).toHaveBeenCalled();
|
|
274
275
|
});
|
|
275
276
|
|
|
277
|
+
it("wraps content in SafeAreaView when safeArea is true", () => {
|
|
278
|
+
const {UNSAFE_root} = renderWithTheme(
|
|
279
|
+
<Page navigation={mockNavigation} safeArea>
|
|
280
|
+
<Text>Content</Text>
|
|
281
|
+
</Page>
|
|
282
|
+
);
|
|
283
|
+
expect(UNSAFE_root.findAllByType(SafeAreaView).length).toBeGreaterThan(0);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("does not wrap content in SafeAreaView when safeArea is omitted", () => {
|
|
287
|
+
const {UNSAFE_root} = renderWithTheme(
|
|
288
|
+
<Page navigation={mockNavigation}>
|
|
289
|
+
<Text>Content</Text>
|
|
290
|
+
</Page>
|
|
291
|
+
);
|
|
292
|
+
expect(UNSAFE_root.findAllByType(SafeAreaView)).toHaveLength(0);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it("does not wrap content in SafeAreaView when safeArea is false", () => {
|
|
296
|
+
const {UNSAFE_root} = renderWithTheme(
|
|
297
|
+
<Page navigation={mockNavigation} safeArea={false}>
|
|
298
|
+
<Text>Content</Text>
|
|
299
|
+
</Page>
|
|
300
|
+
);
|
|
301
|
+
expect(UNSAFE_root.findAllByType(SafeAreaView)).toHaveLength(0);
|
|
302
|
+
});
|
|
303
|
+
|
|
276
304
|
it("safely handles a missing rightButtonOnClick callback", async () => {
|
|
277
305
|
const {getByText} = renderWithTheme(
|
|
278
306
|
<Page navigation={mockNavigation} rightButton="Go" title="Page">
|
package/src/Page.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {router} from "expo-router";
|
|
2
2
|
import React from "react";
|
|
3
|
+
import {SafeAreaView} from "react-native-safe-area-context";
|
|
3
4
|
|
|
4
5
|
import {Box} from "./Box";
|
|
5
6
|
import {Button} from "./Button";
|
|
@@ -11,6 +12,7 @@ import {Spinner} from "./Spinner";
|
|
|
11
12
|
import {Text} from "./Text";
|
|
12
13
|
|
|
13
14
|
export class Page extends React.Component<PageProps, {}> {
|
|
15
|
+
// biome-ignore lint/suspicious/noExplicitAny: ActionSheet class is defined in ActionSheet.tsx which imports from Common.ts indirectly; using its type here would create a circular dependency
|
|
14
16
|
actionSheetRef: React.RefObject<any> = React.createRef();
|
|
15
17
|
|
|
16
18
|
renderHeader() {
|
|
@@ -57,9 +59,9 @@ export class Page extends React.Component<PageProps, {}> {
|
|
|
57
59
|
);
|
|
58
60
|
}
|
|
59
61
|
|
|
60
|
-
|
|
62
|
+
renderBody() {
|
|
61
63
|
return (
|
|
62
|
-
|
|
64
|
+
<>
|
|
63
65
|
<Box
|
|
64
66
|
alignSelf="center"
|
|
65
67
|
avoidKeyboard
|
|
@@ -111,6 +113,20 @@ export class Page extends React.Component<PageProps, {}> {
|
|
|
111
113
|
{this.props.footer}
|
|
112
114
|
</Box>
|
|
113
115
|
)}
|
|
116
|
+
</>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
render() {
|
|
121
|
+
return (
|
|
122
|
+
<ErrorBoundary onError={this.props.onError}>
|
|
123
|
+
{this.props.safeArea ? (
|
|
124
|
+
<SafeAreaView edges={["top", "bottom"]} style={{flex: 1}}>
|
|
125
|
+
{this.renderBody()}
|
|
126
|
+
</SafeAreaView>
|
|
127
|
+
) : (
|
|
128
|
+
this.renderBody()
|
|
129
|
+
)}
|
|
114
130
|
</ErrorBoundary>
|
|
115
131
|
);
|
|
116
132
|
}
|
package/src/Pagination.tsx
CHANGED
package/src/Permissions.ts
CHANGED
|
@@ -10,6 +10,7 @@ export async function requestPermissions(_kind: PermissionKind): Promise<Permiss
|
|
|
10
10
|
// const userPropertyKey = `PermissionsFor${capitalize(kind)}`;
|
|
11
11
|
|
|
12
12
|
// let k = kind;
|
|
13
|
+
// // noExplicitAny: Dead commented-out code; types cannot be resolved without the full uncommented context and Permissions library types
|
|
13
14
|
// let options: any = undefined;
|
|
14
15
|
// if (kind === "locationAlways") {
|
|
15
16
|
// k = "location";
|
|
@@ -21,6 +22,7 @@ export async function requestPermissions(_kind: PermissionKind): Promise<Permiss
|
|
|
21
22
|
|
|
22
23
|
// // TODO check soft request status.
|
|
23
24
|
|
|
25
|
+
// // noExplicitAny: Dead commented-out code; MAP[k] type depends on unreferenced MAP constant
|
|
24
26
|
// const current = await Permissions.check(MAP[k] as any);
|
|
25
27
|
// // Tracking.log(`[permissions] ${k} permissions are ${current}`);
|
|
26
28
|
// if (current === "denied" || current === "limited") {
|
|
@@ -31,6 +33,7 @@ export async function requestPermissions(_kind: PermissionKind): Promise<Permiss
|
|
|
31
33
|
// return resolve("authorized");
|
|
32
34
|
// }
|
|
33
35
|
|
|
36
|
+
// // noExplicitAny: Dead commented-out code; MAP[k] type depends on unreferenced MAP constant
|
|
34
37
|
// const response = await Permissions.request(MAP[k] as any, options);
|
|
35
38
|
// if (response === "granted") {
|
|
36
39
|
// // Tracking.setUserProperty(userPropertyKey, "true");
|
package/src/PickerSelect.tsx
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
Keyboard,
|
|
31
31
|
Modal,
|
|
32
32
|
type ModalProps,
|
|
33
|
+
type NativeSyntheticEvent,
|
|
33
34
|
Platform,
|
|
34
35
|
Pressable,
|
|
35
36
|
type PressableProps,
|
|
@@ -113,7 +114,7 @@ export interface RNPickerSelectProps {
|
|
|
113
114
|
InputAccessoryView?: ComponentType<{testID?: string}>;
|
|
114
115
|
}
|
|
115
116
|
|
|
116
|
-
export
|
|
117
|
+
export const RNPickerSelect = ({
|
|
117
118
|
onValueChange,
|
|
118
119
|
value,
|
|
119
120
|
items,
|
|
@@ -135,11 +136,9 @@ export function RNPickerSelect({
|
|
|
135
136
|
touchableWrapperProps,
|
|
136
137
|
|
|
137
138
|
InputAccessoryView,
|
|
138
|
-
}: RNPickerSelectProps) {
|
|
139
|
+
}: RNPickerSelectProps) => {
|
|
139
140
|
const [showPicker, setShowPicker] = useState<boolean>(false);
|
|
140
|
-
const [animationType, setAnimationType] = useState<"
|
|
141
|
-
undefined
|
|
142
|
-
);
|
|
141
|
+
const [animationType, setAnimationType] = useState<ModalProps["animationType"]>(undefined);
|
|
143
142
|
const [orientation, setOrientation] = useState<"portrait" | "landscape">("portrait");
|
|
144
143
|
const [doneDepressed, setDoneDepressed] = useState<boolean>(false);
|
|
145
144
|
const {theme} = useTheme();
|
|
@@ -185,7 +184,7 @@ export function RNPickerSelect({
|
|
|
185
184
|
}
|
|
186
185
|
return {
|
|
187
186
|
idx,
|
|
188
|
-
selectedItem: options[idx] || {}
|
|
187
|
+
selectedItem: (options[idx] || {}) as Partial<PickerSelectItem>,
|
|
189
188
|
};
|
|
190
189
|
},
|
|
191
190
|
[options]
|
|
@@ -217,9 +216,7 @@ export function RNPickerSelect({
|
|
|
217
216
|
|
|
218
217
|
const onOrientationChange = ({
|
|
219
218
|
nativeEvent,
|
|
220
|
-
}: {
|
|
221
|
-
nativeEvent: {orientation: "portrait" | "landscape"};
|
|
222
|
-
}) => {
|
|
219
|
+
}: NativeSyntheticEvent<{orientation: "portrait" | "landscape"}>) => {
|
|
223
220
|
setOrientation(nativeEvent.orientation);
|
|
224
221
|
};
|
|
225
222
|
|
|
@@ -256,12 +253,13 @@ export function RNPickerSelect({
|
|
|
256
253
|
|
|
257
254
|
const renderPickerItems = () => {
|
|
258
255
|
return options?.map((item) => {
|
|
256
|
+
if (!item) return null;
|
|
259
257
|
return (
|
|
260
258
|
<Picker.Item
|
|
261
|
-
color={item
|
|
262
|
-
key={item
|
|
263
|
-
label={item
|
|
264
|
-
value={item
|
|
259
|
+
color={item.color}
|
|
260
|
+
key={item.key || item.label}
|
|
261
|
+
label={item.label}
|
|
262
|
+
value={item.value}
|
|
265
263
|
/>
|
|
266
264
|
);
|
|
267
265
|
});
|
|
@@ -484,9 +482,14 @@ export function RNPickerSelect({
|
|
|
484
482
|
};
|
|
485
483
|
|
|
486
484
|
const renderAndroidHeadless = () => {
|
|
487
|
-
//
|
|
488
|
-
//
|
|
489
|
-
|
|
485
|
+
// `View` and `Pressable` accept disjoint prop sets; the fork swaps between them to work
|
|
486
|
+
// around an Android touchable bug, so we cast to a structural component type that accepts
|
|
487
|
+
// the union of props actually used in JSX below.
|
|
488
|
+
const Component = (fixAndroidTouchableBug ? View : Pressable) as ComponentType<{
|
|
489
|
+
onPress?: PressableProps["onPress"];
|
|
490
|
+
testID?: string;
|
|
491
|
+
children?: ReactNode;
|
|
492
|
+
}>;
|
|
490
493
|
return (
|
|
491
494
|
<Component onPress={onOpen} testID="android_touchable_wrapper" {...touchableWrapperProps}>
|
|
492
495
|
<View>
|
|
@@ -680,4 +683,4 @@ export function RNPickerSelect({
|
|
|
680
683
|
};
|
|
681
684
|
|
|
682
685
|
return render();
|
|
683
|
-
}
|
|
686
|
+
};
|
package/src/SelectBadge.test.tsx
CHANGED
package/src/SelectField.tsx
CHANGED
|
@@ -19,7 +19,7 @@ export const SelectField: FC<SelectFieldProps> = ({
|
|
|
19
19
|
const clearOption = {label: placeholder ?? "---", value: ""};
|
|
20
20
|
|
|
21
21
|
return (
|
|
22
|
-
<View>
|
|
22
|
+
<View style={{width: "100%"}}>
|
|
23
23
|
{Boolean(title) && <FieldTitle text={title!} />}
|
|
24
24
|
{Boolean(errorText) && <FieldError text={errorText!} />}
|
|
25
25
|
<RNPickerSelect
|
package/src/Signature.test.tsx
CHANGED
package/src/SplitPage.native.tsx
CHANGED
|
@@ -34,6 +34,7 @@ export const SplitPage = ({
|
|
|
34
34
|
const {width} = Dimensions.get("window");
|
|
35
35
|
|
|
36
36
|
const onItemSelect = useCallback(
|
|
37
|
+
// biome-ignore lint/suspicious/noExplicitAny: SplitPage accepts heterogeneous list item shapes from consumers; the generic propagates from listViewData
|
|
37
38
|
async (item: ListRenderItemInfo<any>) => {
|
|
38
39
|
setSelectedId(item.index);
|
|
39
40
|
await onSelectionChange(item);
|
|
@@ -58,6 +59,7 @@ export const SplitPage = ({
|
|
|
58
59
|
return null;
|
|
59
60
|
}
|
|
60
61
|
|
|
62
|
+
// biome-ignore lint/suspicious/noExplicitAny: SplitPage accepts heterogeneous list item shapes from consumers; the generic propagates from listViewData
|
|
61
63
|
const renderItem = (itemInfo: ListRenderItemInfo<any>) => {
|
|
62
64
|
return (
|
|
63
65
|
<Box
|
package/src/SplitPage.tsx
CHANGED
|
@@ -40,6 +40,7 @@ export const SplitPage = ({
|
|
|
40
40
|
const elementArray = Children.toArray(children).filter((c) => c !== null);
|
|
41
41
|
|
|
42
42
|
const onItemSelect = useCallback(
|
|
43
|
+
// biome-ignore lint/suspicious/noExplicitAny: SplitPage accepts heterogeneous list item shapes from consumers; the generic propagates from listViewData
|
|
43
44
|
async (item: ListRenderItemInfo<any>): Promise<void> => {
|
|
44
45
|
setSelectedId(item.index);
|
|
45
46
|
await onSelectionChange(item);
|
|
@@ -69,6 +70,7 @@ export const SplitPage = ({
|
|
|
69
70
|
return null;
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
// biome-ignore lint/suspicious/noExplicitAny: SplitPage accepts heterogeneous list item shapes from consumers; the generic propagates from listViewData
|
|
72
74
|
const renderItem = (itemInfo: ListRenderItemInfo<any>) => {
|
|
73
75
|
return (
|
|
74
76
|
<Box
|
|
@@ -297,7 +299,10 @@ export const SplitPage = ({
|
|
|
297
299
|
padding={2}
|
|
298
300
|
width="100%"
|
|
299
301
|
>
|
|
300
|
-
{loading === true &&
|
|
302
|
+
{loading === true && (
|
|
303
|
+
// biome-ignore lint/suspicious/noExplicitAny: Spinner color is a token enum but the legacy code passes theme.text.primary (a resolved hex string); preserving original behavior
|
|
304
|
+
<Spinner color={theme.text.primary as any} size="md" />
|
|
305
|
+
)}
|
|
301
306
|
{isMobileDevice ? renderMobileSplitPage() : renderSplitPage()}
|
|
302
307
|
</Box>
|
|
303
308
|
);
|
package/src/TapToEdit.test.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// biome-ignore-all lint/suspicious/noExplicitAny: test mock typing
|
|
1
2
|
import {describe, expect, it, mock} from "bun:test";
|
|
2
3
|
import {act, fireEvent} from "@testing-library/react-native";
|
|
3
4
|
import {Linking} from "react-native";
|
|
@@ -211,6 +212,22 @@ describe("TapToEdit", () => {
|
|
|
211
212
|
expect(setValue).toHaveBeenCalled();
|
|
212
213
|
});
|
|
213
214
|
|
|
215
|
+
it("reverts to original value (not edited value) when Cancel is pressed", async () => {
|
|
216
|
+
const setValue = mock((_v: unknown) => {});
|
|
217
|
+
const {getByLabelText, getByText} = renderWithTheme(
|
|
218
|
+
<TapToEdit setValue={setValue} title="Name" value="Jane" />
|
|
219
|
+
);
|
|
220
|
+
await act(async () => {
|
|
221
|
+
fireEvent.press(getByLabelText("Edit"));
|
|
222
|
+
});
|
|
223
|
+
// Simulate user having changed the value during editing
|
|
224
|
+
setValue("Edited Value");
|
|
225
|
+
await act(async () => {
|
|
226
|
+
fireEvent.press(getByText("Cancel"));
|
|
227
|
+
});
|
|
228
|
+
expect(setValue).toHaveBeenLastCalledWith("Jane");
|
|
229
|
+
});
|
|
230
|
+
|
|
214
231
|
it("clears value when Clear button is pressed", async () => {
|
|
215
232
|
const setValue = mock(() => {});
|
|
216
233
|
const onSave = mock(() => Promise.resolve());
|
|
@@ -315,3 +332,34 @@ describe("formatAddress", () => {
|
|
|
315
332
|
expect(result).toContain("(113)");
|
|
316
333
|
});
|
|
317
334
|
});
|
|
335
|
+
|
|
336
|
+
describe("TapToEdit - additional function coverage", () => {
|
|
337
|
+
it("shows Clear button for date type and invokes setValue and onSave", async () => {
|
|
338
|
+
const setValue = mock(() => {});
|
|
339
|
+
const onSave = mock(() => Promise.resolve());
|
|
340
|
+
const {getByLabelText, getByText} = renderWithTheme(
|
|
341
|
+
<TapToEdit onSave={onSave} setValue={setValue} title="Date" type="date" value="2024-01-01" />
|
|
342
|
+
);
|
|
343
|
+
await act(async () => {
|
|
344
|
+
fireEvent.press(getByLabelText("Edit"));
|
|
345
|
+
});
|
|
346
|
+
expect(getByText("Clear")).toBeTruthy();
|
|
347
|
+
await act(async () => {
|
|
348
|
+
fireEvent.press(getByText("Clear"));
|
|
349
|
+
await new Promise((resolve) => setTimeout(resolve, 600));
|
|
350
|
+
});
|
|
351
|
+
expect(setValue).toHaveBeenCalledWith("");
|
|
352
|
+
expect(onSave).toHaveBeenCalledWith("");
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it("assigns inputRef for text type in editing mode", async () => {
|
|
356
|
+
const setValue = mock(() => {});
|
|
357
|
+
const {getByLabelText, queryByText} = renderWithTheme(
|
|
358
|
+
<TapToEdit setValue={setValue} title="Name" type="text" value="Alice" />
|
|
359
|
+
);
|
|
360
|
+
await act(async () => {
|
|
361
|
+
fireEvent.press(getByLabelText("Edit"));
|
|
362
|
+
});
|
|
363
|
+
expect(queryByText("Save")).toBeTruthy();
|
|
364
|
+
});
|
|
365
|
+
});
|
package/src/TapToEdit.tsx
CHANGED
|
@@ -26,7 +26,7 @@ const TapToEditTitle: FC<{
|
|
|
26
26
|
);
|
|
27
27
|
};
|
|
28
28
|
|
|
29
|
-
export
|
|
29
|
+
export const formatAddress = (address: AddressInterface, asString = false): string => {
|
|
30
30
|
let city = "";
|
|
31
31
|
if (address?.city) {
|
|
32
32
|
city = address?.state || address.zipcode ? `${address.city}, ` : `${address.city}`;
|
|
@@ -49,7 +49,6 @@ export function formatAddress(address: AddressInterface, asString = false): stri
|
|
|
49
49
|
const addressLineFour = `${countyName}${address?.countyCode ? ` (${countyCode})` : ""}`;
|
|
50
50
|
|
|
51
51
|
if (!asString) {
|
|
52
|
-
// Only add new lines if lines before and after are not empty to avoid awkward whitespace
|
|
53
52
|
return `${addressLineOne}${
|
|
54
53
|
addressLineOne && (addressLineTwo || addressLineThree) ? `\n` : ""
|
|
55
54
|
}${addressLineTwo}${addressLineTwo && addressLineThree ? `\n` : ""}${addressLineThree}${
|
|
@@ -62,7 +61,7 @@ export function formatAddress(address: AddressInterface, asString = false): stri
|
|
|
62
61
|
addressLineThree && addressLineFour ? `, ` : ""
|
|
63
62
|
}${addressLineFour}`;
|
|
64
63
|
}
|
|
65
|
-
}
|
|
64
|
+
};
|
|
66
65
|
|
|
67
66
|
export const TapToEdit: FC<TapToEditProps> = ({
|
|
68
67
|
value,
|
|
@@ -81,15 +80,11 @@ export const TapToEdit: FC<TapToEditProps> = ({
|
|
|
81
80
|
...fieldProps
|
|
82
81
|
}) => {
|
|
83
82
|
const [editing, setEditing] = useState(false);
|
|
84
|
-
const
|
|
83
|
+
const initialValueRef = useRef<unknown>(undefined);
|
|
85
84
|
const helperText: string | undefined = propsHelperText;
|
|
86
|
-
// setInitialValue is called after initial render to handle the case where the value is updated
|
|
87
|
-
useEffect(() => {
|
|
88
|
-
setInitialValue(value);
|
|
89
|
-
// do not update if value changes
|
|
90
|
-
}, [value]);
|
|
91
85
|
|
|
92
86
|
// TODO: Auto focus on input when editing for field types other than text for accessibility
|
|
87
|
+
// biome-ignore lint/suspicious/noExplicitAny: inputRef references various RN input components (TextInput, etc.) depending on the field type
|
|
93
88
|
const inputRef = useRef<any>(null);
|
|
94
89
|
|
|
95
90
|
// bring the bring the input into focus when editing from within the component,
|
|
@@ -116,13 +111,16 @@ export const TapToEdit: FC<TapToEditProps> = ({
|
|
|
116
111
|
helperText={helperText}
|
|
117
112
|
inputRef={
|
|
118
113
|
["text", "textarea", "url", "email", "number"].includes(fieldProps?.type)
|
|
119
|
-
? (ref:
|
|
114
|
+
? (ref: unknown) => {
|
|
115
|
+
inputRef.current = ref;
|
|
116
|
+
}
|
|
120
117
|
: undefined
|
|
121
118
|
}
|
|
122
119
|
onChange={setValue ?? (() => {})}
|
|
123
120
|
row={fieldProps?.type === "textarea" ? 5 : undefined}
|
|
124
121
|
type={(fieldProps?.type ?? "text") as NonNullable<FieldProps["type"]>}
|
|
125
122
|
value={value}
|
|
123
|
+
// biome-ignore lint/suspicious/noExplicitAny: fieldProps is a discriminated union (FieldProps) but the spread loses narrowing; type-checking each variant individually is impractical here
|
|
126
124
|
{...(fieldProps as any)}
|
|
127
125
|
/>
|
|
128
126
|
{editing && !isEditing && (
|
|
@@ -130,7 +128,7 @@ export const TapToEdit: FC<TapToEditProps> = ({
|
|
|
130
128
|
<Button
|
|
131
129
|
onClick={(): void => {
|
|
132
130
|
if (setValue) {
|
|
133
|
-
setValue(
|
|
131
|
+
setValue(initialValueRef.current);
|
|
134
132
|
}
|
|
135
133
|
setEditing(false);
|
|
136
134
|
}}
|
|
@@ -142,7 +140,6 @@ export const TapToEdit: FC<TapToEditProps> = ({
|
|
|
142
140
|
onClick={(): void => {
|
|
143
141
|
if (setValue) {
|
|
144
142
|
setValue("");
|
|
145
|
-
setInitialValue("" as any);
|
|
146
143
|
}
|
|
147
144
|
if (onSave) {
|
|
148
145
|
onSave("");
|
|
@@ -161,7 +158,6 @@ export const TapToEdit: FC<TapToEditProps> = ({
|
|
|
161
158
|
if (!onSave) {
|
|
162
159
|
console.error("No onSave provided for editable TapToEdit");
|
|
163
160
|
} else {
|
|
164
|
-
setInitialValue(value);
|
|
165
161
|
await onSave(value);
|
|
166
162
|
}
|
|
167
163
|
setEditing(false);
|
|
@@ -271,7 +267,10 @@ export const TapToEdit: FC<TapToEditProps> = ({
|
|
|
271
267
|
accessibilityHint=""
|
|
272
268
|
accessibilityLabel="Edit"
|
|
273
269
|
marginLeft={2}
|
|
274
|
-
onClick={(): void =>
|
|
270
|
+
onClick={(): void => {
|
|
271
|
+
initialValueRef.current = value;
|
|
272
|
+
setEditing(true);
|
|
273
|
+
}}
|
|
275
274
|
width={16}
|
|
276
275
|
>
|
|
277
276
|
<Icon iconName="pencil" size="md" />
|
package/src/Toast.tsx
CHANGED
|
@@ -48,7 +48,7 @@ export const useToast = (): {
|
|
|
48
48
|
};
|
|
49
49
|
return {
|
|
50
50
|
catch: (error: unknown, message?: string, options?: UseToastVariantOptions): void => {
|
|
51
|
-
let exceptionMsg;
|
|
51
|
+
let exceptionMsg: string;
|
|
52
52
|
if (isAPIError(error)) {
|
|
53
53
|
// Get the error without details.
|
|
54
54
|
exceptionMsg = `${message}: ${printAPIError(error)}`;
|