@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.
Files changed (179) hide show
  1. package/dist/ActionSheet.d.ts +5 -5
  2. package/dist/ActionSheet.js +2 -2
  3. package/dist/ActionSheet.js.map +1 -1
  4. package/dist/Avatar.js +1 -1
  5. package/dist/Avatar.js.map +1 -1
  6. package/dist/Banner.js.map +1 -1
  7. package/dist/Box.js +2 -0
  8. package/dist/Box.js.map +1 -1
  9. package/dist/Button.d.ts +2 -2
  10. package/dist/Button.js +35 -23
  11. package/dist/Button.js.map +1 -1
  12. package/dist/Common.d.ts +16 -4
  13. package/dist/Common.js +4 -4
  14. package/dist/Common.js.map +1 -1
  15. package/dist/ConsentFormScreen.js +3 -3
  16. package/dist/ConsentFormScreen.js.map +1 -1
  17. package/dist/ConsentNavigator.d.ts +1 -1
  18. package/dist/ConsentNavigator.js +2 -1
  19. package/dist/ConsentNavigator.js.map +1 -1
  20. package/dist/CustomSelectField.js +3 -1
  21. package/dist/CustomSelectField.js.map +1 -1
  22. package/dist/DataTable.js +1 -1
  23. package/dist/DataTable.js.map +1 -1
  24. package/dist/DateTimeActionSheet.js +2 -1
  25. package/dist/DateTimeActionSheet.js.map +1 -1
  26. package/dist/DateTimeField.js +3 -2
  27. package/dist/DateTimeField.js.map +1 -1
  28. package/dist/DateUtilities.d.ts +25 -25
  29. package/dist/DateUtilities.js +31 -32
  30. package/dist/DateUtilities.js.map +1 -1
  31. package/dist/HeightField.js.map +1 -1
  32. package/dist/Hyperlink.js +19 -9
  33. package/dist/Hyperlink.js.map +1 -1
  34. package/dist/IconButton.js.map +1 -1
  35. package/dist/ImageBackground.d.ts +2 -5
  36. package/dist/ImageBackground.js +1 -1
  37. package/dist/ImageBackground.js.map +1 -1
  38. package/dist/MediaQuery.d.ts +4 -4
  39. package/dist/MediaQuery.js +8 -8
  40. package/dist/MediaQuery.js.map +1 -1
  41. package/dist/ModalSheet.d.ts +3 -2
  42. package/dist/ModalSheet.js +1 -1
  43. package/dist/ModalSheet.js.map +1 -1
  44. package/dist/OfflineBanner.d.ts +21 -0
  45. package/dist/OfflineBanner.js +25 -0
  46. package/dist/OfflineBanner.js.map +1 -0
  47. package/dist/OpenAPIContext.js +1 -1
  48. package/dist/OpenAPIContext.js.map +1 -1
  49. package/dist/Page.d.ts +1 -0
  50. package/dist/Page.js +7 -2
  51. package/dist/Page.js.map +1 -1
  52. package/dist/Pagination.js.map +1 -1
  53. package/dist/Permissions.js +3 -0
  54. package/dist/Permissions.js.map +1 -1
  55. package/dist/PickerSelect.d.ts +1 -1
  56. package/dist/PickerSelect.js +9 -6
  57. package/dist/PickerSelect.js.map +1 -1
  58. package/dist/SelectField.js +1 -1
  59. package/dist/SelectField.js.map +1 -1
  60. package/dist/SplitPage.js +7 -2
  61. package/dist/SplitPage.js.map +1 -1
  62. package/dist/SplitPage.native.js +4 -1
  63. package/dist/SplitPage.native.js.map +1 -1
  64. package/dist/TapToEdit.d.ts +1 -1
  65. package/dist/TapToEdit.js +12 -14
  66. package/dist/TapToEdit.js.map +1 -1
  67. package/dist/Toast.js.map +1 -1
  68. package/dist/ToastNotifications.js +2 -2
  69. package/dist/ToastNotifications.js.map +1 -1
  70. package/dist/Tooltip.d.ts +24 -1
  71. package/dist/Tooltip.js +2 -2
  72. package/dist/Tooltip.js.map +1 -1
  73. package/dist/Unifier.d.ts +3 -3
  74. package/dist/Unifier.js +15 -12
  75. package/dist/Unifier.js.map +1 -1
  76. package/dist/Utilities.d.ts +12 -8
  77. package/dist/Utilities.js +13 -15
  78. package/dist/Utilities.js.map +1 -1
  79. package/dist/index.d.ts +2 -1
  80. package/dist/index.js +2 -1
  81. package/dist/index.js.map +1 -1
  82. package/dist/signUp/PasswordRequirements.js +3 -3
  83. package/dist/signUp/PasswordRequirements.js.map +1 -1
  84. package/dist/table/TableHeaderCell.js +1 -9
  85. package/dist/table/TableHeaderCell.js.map +1 -1
  86. package/dist/table/tableContext.d.ts +1 -1
  87. package/dist/table/tableContext.js +2 -2
  88. package/dist/table/tableContext.js.map +1 -1
  89. package/package.json +2 -1
  90. package/src/ActionSheet.test.tsx +1 -0
  91. package/src/ActionSheet.tsx +8 -6
  92. package/src/Avatar.tsx +9 -2
  93. package/src/Badge.test.tsx +1 -0
  94. package/src/Banner.test.tsx +71 -0
  95. package/src/Banner.tsx +1 -1
  96. package/src/Box.test.tsx +1 -0
  97. package/src/Box.tsx +10 -6
  98. package/src/Button.test.tsx +35 -0
  99. package/src/Button.tsx +65 -34
  100. package/src/Common.ts +42 -19
  101. package/src/ConsentFormScreen.test.tsx +124 -0
  102. package/src/ConsentFormScreen.tsx +18 -6
  103. package/src/ConsentNavigator.test.tsx +1 -0
  104. package/src/ConsentNavigator.tsx +5 -3
  105. package/src/CustomSelectField.tsx +3 -1
  106. package/src/DataTable.test.tsx +218 -0
  107. package/src/DataTable.tsx +1 -1
  108. package/src/DateTimeActionSheet.tsx +7 -3
  109. package/src/DateTimeField.test.tsx +1 -0
  110. package/src/DateTimeField.tsx +3 -2
  111. package/src/DateUtilities.test.ts +111 -0
  112. package/src/DateUtilities.tsx +43 -44
  113. package/src/DecimalRangeActionSheet.test.tsx +28 -0
  114. package/src/ErrorBoundary.test.tsx +1 -0
  115. package/src/HeightActionSheet.test.tsx +16 -0
  116. package/src/HeightField.test.tsx +106 -1
  117. package/src/HeightField.tsx +2 -1
  118. package/src/Hyperlink.tsx +83 -52
  119. package/src/IconButton.tsx +1 -1
  120. package/src/ImageBackground.tsx +5 -6
  121. package/src/MediaQuery.ts +8 -8
  122. package/src/MobileAddressAutoComplete.test.tsx +20 -1
  123. package/src/ModalSheet.test.tsx +1 -5
  124. package/src/ModalSheet.tsx +15 -6
  125. package/src/NumberField.test.tsx +14 -0
  126. package/src/OfflineBanner.test.tsx +70 -0
  127. package/src/OfflineBanner.tsx +54 -0
  128. package/src/OpenAPIContext.tsx +3 -2
  129. package/src/Page.test.tsx +28 -0
  130. package/src/Page.tsx +18 -2
  131. package/src/Pagination.tsx +1 -1
  132. package/src/Permissions.ts +3 -0
  133. package/src/PickerSelect.tsx +20 -17
  134. package/src/SelectBadge.test.tsx +1 -0
  135. package/src/SelectField.tsx +1 -1
  136. package/src/Signature.test.tsx +1 -0
  137. package/src/SplitPage.native.tsx +2 -0
  138. package/src/SplitPage.tsx +6 -1
  139. package/src/TapToEdit.test.tsx +48 -0
  140. package/src/TapToEdit.tsx +13 -14
  141. package/src/Toast.tsx +1 -1
  142. package/src/ToastNotifications.test.tsx +738 -0
  143. package/src/ToastNotifications.tsx +3 -6
  144. package/src/Tooltip.test.tsx +586 -8
  145. package/src/Tooltip.tsx +2 -2
  146. package/src/Unifier.ts +20 -16
  147. package/src/Utilities.tsx +20 -19
  148. package/src/WebAddressAutocomplete.test.tsx +138 -0
  149. package/src/WebDropdownMenu.test.tsx +23 -0
  150. package/src/__snapshots__/AddressField.test.tsx.snap +3 -1
  151. package/src/__snapshots__/Button.test.tsx.snap +92 -50
  152. package/src/__snapshots__/CustomSelectField.test.tsx.snap +21 -7
  153. package/src/__snapshots__/DecimalRangeActionSheet.test.tsx.snap +14 -8
  154. package/src/__snapshots__/ErrorPage.test.tsx.snap +7 -4
  155. package/src/__snapshots__/Field.test.tsx.snap +18 -6
  156. package/src/__snapshots__/HeightActionSheet.test.tsx.snap +14 -8
  157. package/src/__snapshots__/HeightField.test.tsx.snap +35 -20
  158. package/src/__snapshots__/InfoModalIcon.test.tsx.snap +28 -16
  159. package/src/__snapshots__/Modal.test.tsx.snap +19 -10
  160. package/src/__snapshots__/ModalSheet.test.tsx.snap +0 -1
  161. package/src/__snapshots__/NumberPickerActionSheet.test.tsx.snap +14 -8
  162. package/src/__snapshots__/Page.test.tsx.snap +7 -4
  163. package/src/__snapshots__/SelectField.test.tsx.snap +18 -6
  164. package/src/__snapshots__/TerrenoProvider.test.tsx.snap +0 -2
  165. package/src/__snapshots__/TimezonePicker.test.tsx.snap +18 -6
  166. package/src/bunSetup.ts +25 -2
  167. package/src/index.tsx +2 -1
  168. package/src/login/LoginScreen.test.tsx +23 -1
  169. package/src/login/__snapshots__/LoginScreen.test.tsx.snap +15 -6
  170. package/src/signUp/PasswordRequirements.tsx +9 -6
  171. package/src/signUp/__snapshots__/PasswordRequirements.test.tsx.snap +50 -2
  172. package/src/signUp/__snapshots__/SignUpScreen.test.tsx.snap +35 -5
  173. package/src/table/TableHeaderCell.tsx +8 -11
  174. package/src/table/TableRow.test.tsx +31 -1
  175. package/src/table/__snapshots__/TableBadge.test.tsx.snap +3 -1
  176. package/src/table/__snapshots__/TableHeaderCell.test.tsx.snap +2 -0
  177. package/src/table/tableContext.tsx +2 -2
  178. package/src/types/react-native-swiper-flatlist.d.ts +1 -0
  179. 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
+ };
@@ -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 any)?.[dotField];
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: any) => console.error(`Error fetching OpenAPI spec: ${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
- render() {
62
+ renderBody() {
61
63
  return (
62
- <ErrorBoundary onError={this.props.onError}>
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
  }
@@ -134,7 +134,7 @@ export const Pagination: FC<PaginationProps> = ({totalPages, page, setPage}) =>
134
134
  alignItems: "center",
135
135
  display: "flex",
136
136
  flexDirection: "row",
137
- gap: theme.spacing.xs as any,
137
+ gap: theme.spacing.xs,
138
138
  }}
139
139
  >
140
140
  <PaginationButton
@@ -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");
@@ -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 function RNPickerSelect({
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<"none" | "slide" | "fade" | undefined>(
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?.color}
262
- key={item?.key || item?.label}
263
- label={item?.label}
264
- value={item?.value}
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
- // noExplicitAny: Component is View or Pressable depending on a bug workaround flag. View
488
- // ignores Pressable-specific props (onPress) at runtime. A type-safe union cannot express this.
489
- const Component = (fixAndroidTouchableBug ? View : Pressable) as unknown as typeof Pressable;
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
+ };
@@ -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
 
@@ -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
@@ -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 {fireEvent} from "@testing-library/react-native";
3
4
  import {forwardRef, useImperativeHandle} from "react";
@@ -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 && <Spinner color={theme.text.primary as any} size="md" />}
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
  );
@@ -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 function formatAddress(address: AddressInterface, asString = false): string {
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 [initialValue, setInitialValue] = useState();
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: any) => (inputRef.current = 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(initialValue);
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 => setEditing(true)}
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)}`;