@terreno/ui 0.13.0 → 0.14.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.
Files changed (173) hide show
  1. package/dist/ActionSheet.d.ts +4 -4
  2. package/dist/ActionSheet.js.map +1 -1
  3. package/dist/Avatar.js +1 -1
  4. package/dist/Avatar.js.map +1 -1
  5. package/dist/Banner.js.map +1 -1
  6. package/dist/Box.js +2 -0
  7. package/dist/Box.js.map +1 -1
  8. package/dist/Button.d.ts +2 -2
  9. package/dist/Button.js +35 -23
  10. package/dist/Button.js.map +1 -1
  11. package/dist/Common.d.ts +8 -2
  12. package/dist/Common.js.map +1 -1
  13. package/dist/ConsentFormScreen.js +9 -8
  14. package/dist/ConsentFormScreen.js.map +1 -1
  15. package/dist/ConsentNavigator.d.ts +1 -1
  16. package/dist/ConsentNavigator.js +2 -1
  17. package/dist/ConsentNavigator.js.map +1 -1
  18. package/dist/CustomSelectField.js +3 -1
  19. package/dist/CustomSelectField.js.map +1 -1
  20. package/dist/DataTable.js +1 -1
  21. package/dist/DataTable.js.map +1 -1
  22. package/dist/DateTimeActionSheet.js +2 -1
  23. package/dist/DateTimeActionSheet.js.map +1 -1
  24. package/dist/DateTimeField.js +3 -2
  25. package/dist/DateTimeField.js.map +1 -1
  26. package/dist/DateUtilities.js.map +1 -1
  27. package/dist/HeightField.js.map +1 -1
  28. package/dist/Hyperlink.js +19 -9
  29. package/dist/Hyperlink.js.map +1 -1
  30. package/dist/IconButton.js.map +1 -1
  31. package/dist/ImageBackground.d.ts +2 -5
  32. package/dist/ImageBackground.js +1 -1
  33. package/dist/ImageBackground.js.map +1 -1
  34. package/dist/ModalSheet.d.ts +3 -2
  35. package/dist/ModalSheet.js +1 -1
  36. package/dist/ModalSheet.js.map +1 -1
  37. package/dist/OfflineBanner.d.ts +21 -0
  38. package/dist/OfflineBanner.js +25 -0
  39. package/dist/OfflineBanner.js.map +1 -0
  40. package/dist/OpenAPIContext.js +1 -1
  41. package/dist/OpenAPIContext.js.map +1 -1
  42. package/dist/Page.js +1 -0
  43. package/dist/Page.js.map +1 -1
  44. package/dist/Pagination.js.map +1 -1
  45. package/dist/Permissions.js +3 -0
  46. package/dist/Permissions.js.map +1 -1
  47. package/dist/PickerSelect.d.ts +22 -10
  48. package/dist/PickerSelect.js +14 -9
  49. package/dist/PickerSelect.js.map +1 -1
  50. package/dist/SelectBadge.js +11 -1
  51. package/dist/SelectBadge.js.map +1 -1
  52. package/dist/SelectField.js +3 -3
  53. package/dist/SelectField.js.map +1 -1
  54. package/dist/SidebarNavigation.native.js.map +1 -1
  55. package/dist/Signature.js +4 -0
  56. package/dist/Signature.js.map +1 -1
  57. package/dist/Signature.native.js +4 -0
  58. package/dist/Signature.native.js.map +1 -1
  59. package/dist/SplitPage.js +7 -2
  60. package/dist/SplitPage.js.map +1 -1
  61. package/dist/SplitPage.native.js +4 -1
  62. package/dist/SplitPage.native.js.map +1 -1
  63. package/dist/TapToEdit.js +10 -11
  64. package/dist/TapToEdit.js.map +1 -1
  65. package/dist/Theme.d.ts +1 -1
  66. package/dist/Theme.js.map +1 -1
  67. package/dist/Toast.js.map +1 -1
  68. package/dist/ToastNotifications.js.map +1 -1
  69. package/dist/Unifier.d.ts +2 -2
  70. package/dist/Unifier.js +1 -1
  71. package/dist/Unifier.js.map +1 -1
  72. package/dist/Utilities.d.ts +8 -4
  73. package/dist/Utilities.js +1 -1
  74. package/dist/Utilities.js.map +1 -1
  75. package/dist/index.d.ts +1 -0
  76. package/dist/index.js +1 -0
  77. package/dist/index.js.map +1 -1
  78. package/dist/useConsentForms.d.ts +4 -3
  79. package/dist/useConsentForms.js +26 -4
  80. package/dist/useConsentForms.js.map +1 -1
  81. package/dist/useConsentHistory.d.ts +4 -3
  82. package/dist/useConsentHistory.js +26 -4
  83. package/dist/useConsentHistory.js.map +1 -1
  84. package/dist/useSubmitConsent.d.ts +7 -6
  85. package/dist/useSubmitConsent.js +25 -3
  86. package/dist/useSubmitConsent.js.map +1 -1
  87. package/package.json +2 -1
  88. package/src/ActionSheet.test.tsx +1 -0
  89. package/src/ActionSheet.tsx +6 -4
  90. package/src/Avatar.tsx +9 -2
  91. package/src/Badge.test.tsx +1 -0
  92. package/src/Banner.tsx +1 -1
  93. package/src/Box.test.tsx +1 -0
  94. package/src/Box.tsx +10 -6
  95. package/src/Button.test.tsx +35 -0
  96. package/src/Button.tsx +65 -34
  97. package/src/Common.ts +32 -15
  98. package/src/ConsentFormScreen.test.tsx +149 -0
  99. package/src/ConsentFormScreen.tsx +20 -3
  100. package/src/ConsentNavigator.test.tsx +1 -0
  101. package/src/ConsentNavigator.tsx +5 -3
  102. package/src/CustomSelectField.tsx +3 -1
  103. package/src/DataTable.test.tsx +1 -0
  104. package/src/DataTable.tsx +1 -1
  105. package/src/DateTimeActionSheet.tsx +7 -3
  106. package/src/DateTimeField.test.tsx +1 -0
  107. package/src/DateTimeField.tsx +3 -2
  108. package/src/DateUtilities.test.ts +111 -0
  109. package/src/DateUtilities.tsx +6 -6
  110. package/src/DecimalRangeActionSheet.test.tsx +28 -0
  111. package/src/ErrorBoundary.test.tsx +1 -0
  112. package/src/HeightField.test.tsx +68 -0
  113. package/src/HeightField.tsx +2 -1
  114. package/src/Hyperlink.tsx +83 -52
  115. package/src/IconButton.tsx +1 -1
  116. package/src/ImageBackground.tsx +5 -6
  117. package/src/Modal.tsx +2 -2
  118. package/src/ModalSheet.test.tsx +1 -5
  119. package/src/ModalSheet.tsx +15 -6
  120. package/src/NumberField.test.tsx +14 -0
  121. package/src/OfflineBanner.test.tsx +70 -0
  122. package/src/OfflineBanner.tsx +54 -0
  123. package/src/OpenAPIContext.tsx +3 -2
  124. package/src/Page.tsx +1 -0
  125. package/src/Pagination.tsx +1 -1
  126. package/src/Permissions.ts +3 -0
  127. package/src/PickerSelect.tsx +48 -31
  128. package/src/SelectBadge.test.tsx +1 -0
  129. package/src/SelectBadge.tsx +7 -3
  130. package/src/SelectField.tsx +3 -3
  131. package/src/SidebarNavigation.native.tsx +6 -2
  132. package/src/Signature.native.tsx +4 -0
  133. package/src/Signature.test.tsx +11 -0
  134. package/src/Signature.tsx +4 -0
  135. package/src/SplitPage.native.tsx +2 -0
  136. package/src/SplitPage.tsx +6 -1
  137. package/src/TapToEdit.test.tsx +17 -0
  138. package/src/TapToEdit.tsx +11 -11
  139. package/src/Theme.tsx +17 -14
  140. package/src/Toast.tsx +1 -1
  141. package/src/ToastNotifications.tsx +1 -4
  142. package/src/Tooltip.test.tsx +40 -28
  143. package/src/Unifier.ts +6 -5
  144. package/src/Utilities.tsx +9 -6
  145. package/src/__snapshots__/AddressField.test.tsx.snap +3 -2
  146. package/src/__snapshots__/Button.test.tsx.snap +92 -50
  147. package/src/__snapshots__/CustomSelectField.test.tsx.snap +21 -14
  148. package/src/__snapshots__/DecimalRangeActionSheet.test.tsx.snap +14 -8
  149. package/src/__snapshots__/ErrorPage.test.tsx.snap +7 -4
  150. package/src/__snapshots__/Field.test.tsx.snap +18 -12
  151. package/src/__snapshots__/HeightActionSheet.test.tsx.snap +14 -8
  152. package/src/__snapshots__/HeightField.test.tsx.snap +35 -20
  153. package/src/__snapshots__/InfoModalIcon.test.tsx.snap +28 -16
  154. package/src/__snapshots__/Modal.test.tsx.snap +19 -10
  155. package/src/__snapshots__/ModalSheet.test.tsx.snap +0 -1
  156. package/src/__snapshots__/NumberPickerActionSheet.test.tsx.snap +14 -8
  157. package/src/__snapshots__/Page.test.tsx.snap +7 -4
  158. package/src/__snapshots__/PickerSelect.test.tsx.snap +0 -7
  159. package/src/__snapshots__/SelectField.test.tsx.snap +18 -12
  160. package/src/__snapshots__/TerrenoProvider.test.tsx.snap +2 -18
  161. package/src/__snapshots__/TimezonePicker.test.tsx.snap +18 -12
  162. package/src/bunSetup.ts +45 -0
  163. package/src/index.tsx +1 -0
  164. package/src/login/__snapshots__/LoginScreen.test.tsx.snap +15 -6
  165. package/src/signUp/__snapshots__/SignUpScreen.test.tsx.snap +10 -4
  166. package/src/table/__snapshots__/TableBadge.test.tsx.snap +3 -2
  167. package/src/types/react-native-swiper-flatlist.d.ts +1 -0
  168. package/src/useConsentForms.test.ts +25 -0
  169. package/src/useConsentForms.ts +32 -7
  170. package/src/useConsentHistory.test.ts +99 -0
  171. package/src/useConsentHistory.ts +31 -6
  172. package/src/useSubmitConsent.test.ts +24 -0
  173. package/src/useSubmitConsent.ts +35 -10
@@ -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");
@@ -25,15 +25,19 @@
25
25
 
26
26
  import {Picker} from "@react-native-picker/picker";
27
27
  import isEqual from "lodash/isEqual";
28
- import {useCallback, useEffect, useMemo, useState} from "react";
28
+ import {type ComponentType, type ReactNode, useCallback, useEffect, useMemo, useState} from "react";
29
29
  import {
30
30
  Keyboard,
31
31
  Modal,
32
+ type ModalProps,
33
+ type NativeSyntheticEvent,
32
34
  Platform,
33
35
  Pressable,
36
+ type PressableProps,
34
37
  StyleSheet,
35
38
  Text,
36
39
  TextInput,
40
+ type TextInputProps,
37
41
  TouchableOpacity,
38
42
  View,
39
43
  } from "react-native";
@@ -67,14 +71,23 @@ export const defaultStyles = StyleSheet.create({
67
71
  },
68
72
  });
69
73
 
74
+ /** A single option for the picker select component. */
75
+ export interface PickerSelectItem {
76
+ label: string;
77
+ value: string | number | null;
78
+ key?: string | number;
79
+ color?: string;
80
+ inputLabel?: string;
81
+ }
82
+
70
83
  export interface RNPickerSelectProps {
71
- onValueChange: (value: any, index: any) => void;
72
- items: any[];
73
- value?: any;
74
- placeholder?: any;
84
+ onValueChange: (value: string | number | null, index: number) => void;
85
+ items: PickerSelectItem[];
86
+ value?: string | number | null;
87
+ placeholder?: Partial<PickerSelectItem>;
75
88
  disabled?: boolean;
76
89
  itemKey?: string | number;
77
- children?: any;
90
+ children?: ReactNode;
78
91
  onOpen?: () => void;
79
92
  useNativeAndroidPickerStyle?: boolean;
80
93
  fixAndroidTouchableBug?: boolean;
@@ -87,18 +100,18 @@ export interface RNPickerSelectProps {
87
100
  onClose?: () => void;
88
101
 
89
102
  // Modal props (iOS only)
90
- modalProps?: any;
103
+ modalProps?: Partial<ModalProps>;
91
104
 
92
105
  // TextInput props
93
- textInputProps?: any;
106
+ textInputProps?: Partial<TextInputProps>;
94
107
 
95
108
  // Touchable Done props (iOS only)
96
- touchableDoneProps?: any;
109
+ touchableDoneProps?: Partial<PressableProps>;
97
110
 
98
111
  // Touchable wrapper props
99
- touchableWrapperProps?: any;
112
+ touchableWrapperProps?: Partial<PressableProps>;
100
113
 
101
- InputAccessoryView?: any;
114
+ InputAccessoryView?: ComponentType<{testID?: string}>;
102
115
  }
103
116
 
104
117
  export function RNPickerSelect({
@@ -125,7 +138,7 @@ export function RNPickerSelect({
125
138
  InputAccessoryView,
126
139
  }: RNPickerSelectProps) {
127
140
  const [showPicker, setShowPicker] = useState<boolean>(false);
128
- const [animationType, setAnimationType] = useState(undefined);
141
+ const [animationType, setAnimationType] = useState<ModalProps["animationType"]>(undefined);
129
142
  const [orientation, setOrientation] = useState<"portrait" | "landscape">("portrait");
130
143
  const [doneDepressed, setDoneDepressed] = useState<boolean>(false);
131
144
  const {theme} = useTheme();
@@ -159,25 +172,25 @@ export function RNPickerSelect({
159
172
  }, [items, placeholder]);
160
173
 
161
174
  const getSelectedItem = useCallback(
162
- (key: any, val: any) => {
163
- let idx = options.findIndex((item: any) => {
164
- if (item.key && key) {
175
+ (key: string | number | undefined, val: string | number | null | undefined) => {
176
+ let idx = options.findIndex((item) => {
177
+ if (item?.key && key) {
165
178
  return isEqual(item.key, key);
166
179
  }
167
- return isEqual(item.value, val);
180
+ return isEqual(item?.value, val);
168
181
  });
169
182
  if (idx === -1) {
170
183
  idx = 0;
171
184
  }
172
185
  return {
173
186
  idx,
174
- selectedItem: options[idx] || {},
187
+ selectedItem: (options[idx] || {}) as Partial<PickerSelectItem>,
175
188
  };
176
189
  },
177
190
  [options]
178
191
  );
179
192
 
180
- const [selectedItem, setSelectedItem] = useState<any>(() => {
193
+ const [selectedItem, setSelectedItem] = useState<Partial<PickerSelectItem>>(() => {
181
194
  return getSelectedItem(itemKey, value).selectedItem;
182
195
  });
183
196
 
@@ -195,13 +208,15 @@ export function RNPickerSelect({
195
208
  togglePicker(false, onDownArrow);
196
209
  };
197
210
 
198
- const onValueChangeEvent = (val: any, index: any) => {
211
+ const onValueChangeEvent = (val: string | number | null, index: number) => {
199
212
  const item = getSelectedItem(itemKey, val);
200
213
  onValueChange(val, index);
201
214
  setSelectedItem(item.selectedItem);
202
215
  };
203
216
 
204
- const onOrientationChange = ({nativeEvent}: any) => {
217
+ const onOrientationChange = ({
218
+ nativeEvent,
219
+ }: NativeSyntheticEvent<{orientation: "portrait" | "landscape"}>) => {
205
220
  setOrientation(nativeEvent.orientation);
206
221
  };
207
222
 
@@ -215,7 +230,7 @@ export function RNPickerSelect({
215
230
  }
216
231
  };
217
232
 
218
- const togglePicker = (animate = false, postToggleCallback?: any) => {
233
+ const togglePicker = (animate = false, postToggleCallback?: () => void) => {
219
234
  if (disabled) {
220
235
  return;
221
236
  }
@@ -237,7 +252,8 @@ export function RNPickerSelect({
237
252
  };
238
253
 
239
254
  const renderPickerItems = () => {
240
- return options?.map((item: any) => {
255
+ return options?.map((item) => {
256
+ if (!item) return null;
241
257
  return (
242
258
  <Picker.Item
243
259
  color={item.color}
@@ -408,7 +424,6 @@ export function RNPickerSelect({
408
424
  ]}
409
425
  >
410
426
  <Pressable
411
- activeOpacity={1}
412
427
  onPress={() => {
413
428
  togglePicker(true);
414
429
  }}
@@ -467,14 +482,16 @@ export function RNPickerSelect({
467
482
  };
468
483
 
469
484
  const renderAndroidHeadless = () => {
470
- const Component: any = fixAndroidTouchableBug ? View : 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
+ }>;
471
493
  return (
472
- <Component
473
- activeOpacity={1}
474
- onPress={onOpen}
475
- testID="android_touchable_wrapper"
476
- {...touchableWrapperProps}
477
- >
494
+ <Component onPress={onOpen} testID="android_touchable_wrapper" {...touchableWrapperProps}>
478
495
  <View>
479
496
  {renderTextInputOrChildren()}
480
497
  <Picker
@@ -636,7 +653,7 @@ export function RNPickerSelect({
636
653
  // Pass the original (non-stringified) value through so lodash
637
654
  // `isEqual` matching in `getSelectedItem` works for number /
638
655
  // object values.
639
- const originalValue = options[originalIndex]?.value;
656
+ const originalValue = options[originalIndex]?.value ?? null;
640
657
  onValueChangeEvent(originalValue, originalIndex);
641
658
  closeWebMenu();
642
659
  }}
@@ -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
 
@@ -3,7 +3,7 @@ import type React from "react";
3
3
  import {useCallback, useMemo, useState} from "react";
4
4
  import {Modal, Platform, Text, TouchableOpacity, View} from "react-native";
5
5
 
6
- import type {FieldOption, SelectBadgeProps, SurfaceTheme, TextTheme} from "./Common";
6
+ import type {FieldOption, IconColor, SelectBadgeProps, SurfaceTheme, TextTheme} from "./Common";
7
7
  import {Icon} from "./Icon";
8
8
  import {useTheme} from "./Theme";
9
9
  import {useWebDropdownAnchor, WebDropdownMenu, type WebDropdownMenuOption} from "./WebDropdownMenu";
@@ -98,7 +98,7 @@ export const SelectBadge = ({
98
98
  );
99
99
 
100
100
  const renderPickerItems = useCallback(() => {
101
- return options?.map((item: any) => (
101
+ return options?.map((item: FieldOption) => (
102
102
  <Picker.Item key={item.key || item.label} label={item.label} value={item.value} />
103
103
  ));
104
104
  }, [options]);
@@ -321,7 +321,11 @@ export const SelectBadge = ({
321
321
  }}
322
322
  >
323
323
  <Icon
324
- color={textColor as any}
324
+ // noExplicitAny: textColor is a resolved hex string from the theme, but Icon's
325
+ // color prop expects an IconColor key. Icon falls back to the raw string at runtime
326
+ // (theme.text[color] ?? color), but the type cannot be narrowed without changing
327
+ // Icon's type signature to accept arbitrary strings.
328
+ color={textColor as unknown as IconColor}
325
329
  iconName={showPicker ? "chevron-up" : "chevron-down"}
326
330
  size="sm"
327
331
  />
@@ -19,17 +19,17 @@ 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
26
26
  disabled={disabled}
27
27
  items={options}
28
28
  onValueChange={(v) => {
29
- if (v === undefined || v === "") {
29
+ if (v === undefined || v === null || v === "") {
30
30
  onChange("");
31
31
  } else {
32
- onChange(v);
32
+ onChange(String(v));
33
33
  }
34
34
  }}
35
35
  placeholder={!requireValue ? clearOption : {}}
@@ -5,7 +5,7 @@ import {Navigator, Slot} from "expo-router";
5
5
  // update the import path here — this is the only place in the codebase that references it.
6
6
  // eslint-disable-next-line import/no-internal-modules
7
7
  import {Screen} from "expo-router/build/views/Screen";
8
- import {type FC, useCallback, useEffect, useMemo, useRef, useState} from "react";
8
+ import {type FC, type ReactNode, useCallback, useEffect, useMemo, useRef, useState} from "react";
9
9
  import {
10
10
  Animated,
11
11
  Dimensions,
@@ -361,7 +361,11 @@ const SidebarHeader: FC<{onOpen: () => void}> = ({onOpen}) => {
361
361
  const insets = useSafeAreaInsets();
362
362
  const {state, descriptors} = Navigator.useContext();
363
363
  const activeRoute = state.routes[state.index];
364
- const {headerLeft, headerRight, title} = (descriptors[activeRoute?.key]?.options ?? {}) as any;
364
+ const {headerLeft, headerRight, title} = (descriptors[activeRoute?.key]?.options ?? {}) as {
365
+ headerLeft?: (props: object) => ReactNode;
366
+ headerRight?: (props: object) => ReactNode;
367
+ title?: string;
368
+ };
365
369
 
366
370
  return (
367
371
  <View
@@ -18,6 +18,10 @@ export const Signature: FC<Props> = ({onChange, onStart, onEnd}: Props) => {
18
18
 
19
19
  const handleClear = () => {
20
20
  ref.current?.clearSignature();
21
+ // `clearSignature` on the underlying canvas does not fire `onOK`, so the
22
+ // parent never learns the signature is gone. Push an empty value so any
23
+ // "signature required" gating reflects the cleared state immediately.
24
+ onChange("");
21
25
  };
22
26
 
23
27
  const onBegin = () => {
@@ -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";
@@ -49,6 +50,16 @@ describe("Signature", () => {
49
50
  expect(clearMock).toHaveBeenCalledTimes(1);
50
51
  });
51
52
 
53
+ it("notifies the parent with an empty value when Clear is pressed", () => {
54
+ clearMock.mockClear();
55
+ const mockOnChange = mock(() => {});
56
+ const {getByText} = renderWithTheme(<Signature onChange={mockOnChange} />);
57
+ fireEvent.press(getByText("Clear"));
58
+ // Without this, "signature required" gating in parents would never reset
59
+ // because the underlying canvas clear() does not fire onEnd/onOK.
60
+ expect(mockOnChange).toHaveBeenCalledWith("");
61
+ });
62
+
52
63
  it("calls onChange with the data URL when a stroke ends", () => {
53
64
  toDataURLReturn = "data:image/png;base64,abc";
54
65
  const mockOnChange = mock(() => {});
package/src/Signature.tsx CHANGED
@@ -17,6 +17,10 @@ export const Signature = ({onChange}: SignatureProps): ReactElement | null => {
17
17
 
18
18
  const onClear = () => {
19
19
  ref.current?.clear();
20
+ // `clear()` on the underlying canvas does not fire `onEnd`, so the parent
21
+ // never learns the signature is gone. Push an empty value so any
22
+ // "signature required" gating reflects the cleared state immediately.
23
+ onChange("");
20
24
  };
21
25
 
22
26
  const onUpdatedSignature = () => {
@@ -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());
package/src/TapToEdit.tsx CHANGED
@@ -81,15 +81,11 @@ export const TapToEdit: FC<TapToEditProps> = ({
81
81
  ...fieldProps
82
82
  }) => {
83
83
  const [editing, setEditing] = useState(false);
84
- const [initialValue, setInitialValue] = useState();
84
+ const initialValueRef = useRef<unknown>(undefined);
85
85
  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
86
 
92
87
  // TODO: Auto focus on input when editing for field types other than text for accessibility
88
+ // biome-ignore lint/suspicious/noExplicitAny: inputRef references various RN input components (TextInput, etc.) depending on the field type
93
89
  const inputRef = useRef<any>(null);
94
90
 
95
91
  // bring the bring the input into focus when editing from within the component,
@@ -116,13 +112,16 @@ export const TapToEdit: FC<TapToEditProps> = ({
116
112
  helperText={helperText}
117
113
  inputRef={
118
114
  ["text", "textarea", "url", "email", "number"].includes(fieldProps?.type)
119
- ? (ref: any) => (inputRef.current = ref)
115
+ ? (ref: unknown) => {
116
+ inputRef.current = ref;
117
+ }
120
118
  : undefined
121
119
  }
122
120
  onChange={setValue ?? (() => {})}
123
121
  row={fieldProps?.type === "textarea" ? 5 : undefined}
124
122
  type={(fieldProps?.type ?? "text") as NonNullable<FieldProps["type"]>}
125
123
  value={value}
124
+ // 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
125
  {...(fieldProps as any)}
127
126
  />
128
127
  {editing && !isEditing && (
@@ -130,7 +129,7 @@ export const TapToEdit: FC<TapToEditProps> = ({
130
129
  <Button
131
130
  onClick={(): void => {
132
131
  if (setValue) {
133
- setValue(initialValue);
132
+ setValue(initialValueRef.current);
134
133
  }
135
134
  setEditing(false);
136
135
  }}
@@ -142,7 +141,6 @@ export const TapToEdit: FC<TapToEditProps> = ({
142
141
  onClick={(): void => {
143
142
  if (setValue) {
144
143
  setValue("");
145
- setInitialValue("" as any);
146
144
  }
147
145
  if (onSave) {
148
146
  onSave("");
@@ -161,7 +159,6 @@ export const TapToEdit: FC<TapToEditProps> = ({
161
159
  if (!onSave) {
162
160
  console.error("No onSave provided for editable TapToEdit");
163
161
  } else {
164
- setInitialValue(value);
165
162
  await onSave(value);
166
163
  }
167
164
  setEditing(false);
@@ -271,7 +268,10 @@ export const TapToEdit: FC<TapToEditProps> = ({
271
268
  accessibilityHint=""
272
269
  accessibilityLabel="Edit"
273
270
  marginLeft={2}
274
- onClick={(): void => setEditing(true)}
271
+ onClick={(): void => {
272
+ initialValueRef.current = value;
273
+ setEditing(true);
274
+ }}
275
275
  width={16}
276
276
  >
277
277
  <Icon iconName="pencil" size="md" />
package/src/Theme.tsx CHANGED
@@ -172,18 +172,21 @@ const computeTheme = (
172
172
  return acc;
173
173
  }
174
174
  const value = themeConfig[key as keyof TerrenoThemeConfig] ?? {};
175
- acc[key as keyof TerrenoTheme] = Object.keys(value).reduce((accKey, valueKey) => {
176
- const primitiveKey = value[valueKey as keyof typeof value] as keyof ThemePrimitives;
177
- if (key === "font") {
178
- accKey[valueKey] = primitiveKey;
179
- } else {
180
- if (primitives[primitiveKey] === undefined) {
181
- console.error(`Primitive ${primitiveKey} not found in theme.`);
175
+ (acc as unknown as Record<string, unknown>)[key] = Object.keys(value).reduce(
176
+ (accKey, valueKey) => {
177
+ const primitiveKey = value[valueKey as keyof typeof value] as keyof ThemePrimitives;
178
+ if (key === "font") {
179
+ accKey[valueKey] = primitiveKey;
180
+ } else {
181
+ if (primitives[primitiveKey] === undefined) {
182
+ console.error(`Primitive ${primitiveKey} not found in theme.`);
183
+ }
184
+ accKey[valueKey] = primitives[primitiveKey];
182
185
  }
183
- accKey[valueKey as keyof typeof accKey] = primitives[primitiveKey];
184
- }
185
- return accKey;
186
- }, {} as any);
186
+ return accKey;
187
+ },
188
+ {} as Record<string, string | number>
189
+ );
187
190
  return acc;
188
191
  }, {} as TerrenoTheme);
189
192
  return {...theme, primitives};
@@ -199,7 +202,7 @@ export const ThemeContext = createContext({
199
202
  });
200
203
 
201
204
  interface ThemeProviderProps {
202
- children: any;
205
+ children: React.ReactNode;
203
206
  }
204
207
 
205
208
  export const ThemeProvider = ({children}: ThemeProviderProps) => {
@@ -225,12 +228,12 @@ export const ThemeProvider = ({children}: ThemeProviderProps) => {
225
228
  const prevSubTheme = prev[key as keyof TerrenoThemeConfig];
226
229
 
227
230
  if (newSubTheme && typeof newSubTheme === "object") {
228
- (mergedTheme as any)[key as keyof TerrenoThemeConfig] = {
231
+ (mergedTheme as Record<string, unknown>)[key] = {
229
232
  ...prevSubTheme,
230
233
  ...newSubTheme,
231
234
  };
232
235
  } else {
233
- mergedTheme[key as keyof TerrenoThemeConfig] = newSubTheme as any;
236
+ (mergedTheme as Record<string, unknown>)[key] = newSubTheme;
234
237
  }
235
238
  }
236
239
  }
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)}`;
@@ -177,10 +177,7 @@ export interface ToastOptions {
177
177
  /**
178
178
  * Payload data for custom toasts. You can pass whatever you want
179
179
  */
180
- // noExplicitAny: This is the public API of a vendored 3rd-party library
181
- // (react-native-toast-notifications); the `data` field is an opaque
182
- // user-provided payload. Tightening to `unknown` breaks downstream consumers
183
- // that spread `data` into `Toast` (e.g. TerrenoProvider) without refactor.
180
+ // biome-ignore lint/suspicious/noExplicitAny: This is the public API of a vendored 3rd-party library (react-native-toast-notifications); the data field is an opaque user-provided payload. Tightening to unknown breaks downstream consumers that spread data into Toast (e.g. TerrenoProvider) without refactor.
184
181
  data?: any;
185
182
 
186
183
  swipeEnabled?: boolean;