@terreno/ui 0.13.3 → 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 (140) 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 +1 -1
  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.js +7 -4
  48. package/dist/PickerSelect.js.map +1 -1
  49. package/dist/SelectField.js +1 -1
  50. package/dist/SelectField.js.map +1 -1
  51. package/dist/SplitPage.js +7 -2
  52. package/dist/SplitPage.js.map +1 -1
  53. package/dist/SplitPage.native.js +4 -1
  54. package/dist/SplitPage.native.js.map +1 -1
  55. package/dist/TapToEdit.js +10 -11
  56. package/dist/TapToEdit.js.map +1 -1
  57. package/dist/Toast.js.map +1 -1
  58. package/dist/ToastNotifications.js.map +1 -1
  59. package/dist/Unifier.d.ts +2 -2
  60. package/dist/Unifier.js +1 -1
  61. package/dist/Unifier.js.map +1 -1
  62. package/dist/Utilities.d.ts +8 -4
  63. package/dist/Utilities.js +1 -1
  64. package/dist/Utilities.js.map +1 -1
  65. package/dist/index.d.ts +1 -0
  66. package/dist/index.js +1 -0
  67. package/dist/index.js.map +1 -1
  68. package/package.json +2 -1
  69. package/src/ActionSheet.test.tsx +1 -0
  70. package/src/ActionSheet.tsx +6 -4
  71. package/src/Avatar.tsx +9 -2
  72. package/src/Badge.test.tsx +1 -0
  73. package/src/Banner.tsx +1 -1
  74. package/src/Box.test.tsx +1 -0
  75. package/src/Box.tsx +10 -6
  76. package/src/Button.test.tsx +35 -0
  77. package/src/Button.tsx +65 -34
  78. package/src/Common.ts +32 -15
  79. package/src/ConsentFormScreen.test.tsx +102 -0
  80. package/src/ConsentFormScreen.tsx +9 -3
  81. package/src/ConsentNavigator.test.tsx +1 -0
  82. package/src/ConsentNavigator.tsx +5 -3
  83. package/src/CustomSelectField.tsx +3 -1
  84. package/src/DataTable.test.tsx +1 -0
  85. package/src/DataTable.tsx +1 -1
  86. package/src/DateTimeActionSheet.tsx +7 -3
  87. package/src/DateTimeField.test.tsx +1 -0
  88. package/src/DateTimeField.tsx +3 -2
  89. package/src/DateUtilities.test.ts +111 -0
  90. package/src/DateUtilities.tsx +6 -6
  91. package/src/DecimalRangeActionSheet.test.tsx +28 -0
  92. package/src/ErrorBoundary.test.tsx +1 -0
  93. package/src/HeightField.tsx +2 -1
  94. package/src/Hyperlink.tsx +83 -52
  95. package/src/IconButton.tsx +1 -1
  96. package/src/ImageBackground.tsx +5 -6
  97. package/src/ModalSheet.test.tsx +1 -5
  98. package/src/ModalSheet.tsx +15 -6
  99. package/src/NumberField.test.tsx +14 -0
  100. package/src/OfflineBanner.test.tsx +70 -0
  101. package/src/OfflineBanner.tsx +54 -0
  102. package/src/OpenAPIContext.tsx +3 -2
  103. package/src/Page.tsx +1 -0
  104. package/src/Pagination.tsx +1 -1
  105. package/src/Permissions.ts +3 -0
  106. package/src/PickerSelect.tsx +17 -14
  107. package/src/SelectBadge.test.tsx +1 -0
  108. package/src/SelectField.tsx +1 -1
  109. package/src/Signature.test.tsx +1 -0
  110. package/src/SplitPage.native.tsx +2 -0
  111. package/src/SplitPage.tsx +6 -1
  112. package/src/TapToEdit.test.tsx +17 -0
  113. package/src/TapToEdit.tsx +11 -11
  114. package/src/Toast.tsx +1 -1
  115. package/src/ToastNotifications.tsx +1 -4
  116. package/src/Tooltip.test.tsx +0 -7
  117. package/src/Unifier.ts +6 -5
  118. package/src/Utilities.tsx +9 -6
  119. package/src/__snapshots__/AddressField.test.tsx.snap +3 -1
  120. package/src/__snapshots__/Button.test.tsx.snap +92 -50
  121. package/src/__snapshots__/CustomSelectField.test.tsx.snap +21 -7
  122. package/src/__snapshots__/DecimalRangeActionSheet.test.tsx.snap +14 -8
  123. package/src/__snapshots__/ErrorPage.test.tsx.snap +7 -4
  124. package/src/__snapshots__/Field.test.tsx.snap +18 -6
  125. package/src/__snapshots__/HeightActionSheet.test.tsx.snap +14 -8
  126. package/src/__snapshots__/HeightField.test.tsx.snap +35 -20
  127. package/src/__snapshots__/InfoModalIcon.test.tsx.snap +28 -16
  128. package/src/__snapshots__/Modal.test.tsx.snap +19 -10
  129. package/src/__snapshots__/ModalSheet.test.tsx.snap +0 -1
  130. package/src/__snapshots__/NumberPickerActionSheet.test.tsx.snap +14 -8
  131. package/src/__snapshots__/Page.test.tsx.snap +7 -4
  132. package/src/__snapshots__/SelectField.test.tsx.snap +18 -6
  133. package/src/__snapshots__/TerrenoProvider.test.tsx.snap +0 -2
  134. package/src/__snapshots__/TimezonePicker.test.tsx.snap +18 -6
  135. package/src/bunSetup.ts +25 -2
  136. package/src/index.tsx +1 -0
  137. package/src/login/__snapshots__/LoginScreen.test.tsx.snap +15 -6
  138. package/src/signUp/__snapshots__/SignUpScreen.test.tsx.snap +10 -4
  139. package/src/table/__snapshots__/TableBadge.test.tsx.snap +3 -1
  140. package/src/types/react-native-swiper-flatlist.d.ts +1 -0
package/src/Common.ts CHANGED
@@ -4,6 +4,7 @@ import type {ReactElement, ReactNode} from "react";
4
4
  import type {
5
5
  ImageStyle,
6
6
  ListRenderItemInfo,
7
+ ScrollView,
7
8
  StyleProp,
8
9
  TextInput,
9
10
  TextStyle,
@@ -463,7 +464,7 @@ export interface BoxPropsBase {
463
464
  lgColumn?: UnsignedUpTo12;
464
465
  dangerouslySetInlineStyle?: {
465
466
  __style: {
466
- // noExplicitAny: escape hatch for arbitrary inline style values
467
+ // biome-ignore lint/suspicious/noExplicitAny: escape hatch for arbitrary inline style values that users may need to set
467
468
  [key: string]: any;
468
469
  };
469
470
  };
@@ -549,7 +550,7 @@ export interface BoxPropsBase {
549
550
 
550
551
  avoidKeyboard?: boolean;
551
552
  keyboardOffset?: number;
552
- scrollRef?: React.RefObject<any>;
553
+ scrollRef?: React.RefObject<ScrollView | null>;
553
554
  onScroll?: (offsetY: number) => void;
554
555
  onLayout?: (event: LayoutChangeEvent) => void;
555
556
  testID?: string;
@@ -858,17 +859,17 @@ export interface SplitPageProps {
858
859
  loading?: boolean;
859
860
  color?: SurfaceColor;
860
861
  keyboardOffset?: number;
861
- // noExplicitAny: ListRenderItemInfo generic type depends on the consumer's data shape
862
+ // biome-ignore lint/suspicious/noExplicitAny: ListRenderItemInfo generic type depends on the consumer's data shape
862
863
  renderListViewItem: (itemInfo: ListRenderItemInfo<any>) => ReactElement | null;
863
864
  renderListViewHeader?: () => ReactElement | null;
864
865
  renderContent?: (index?: number) => ReactElement | ReactElement[] | null;
865
- // noExplicitAny: list data type varies by consumer's data model
866
+ // biome-ignore lint/suspicious/noExplicitAny: list data type varies by consumer's data model
866
867
  listViewData: any[];
867
868
  listViewExtraData?: unknown;
868
869
  listViewWidth?: number;
869
870
  listViewMaxWidth?: number;
870
871
  renderChild?: () => ReactChild;
871
- // noExplicitAny: callback value type varies by consumer's data model
872
+ // biome-ignore lint/suspicious/noExplicitAny: callback value type varies by consumer's data model
872
873
  onSelectionChange?: (value?: any) => void | Promise<void>;
873
874
  }
874
875
 
@@ -1500,6 +1501,8 @@ export interface BodyProps {
1500
1501
  children?: ReactNode;
1501
1502
  }
1502
1503
 
1504
+ export type ButtonPressAnimation = "scale" | "opacity" | "none";
1505
+
1503
1506
  export interface ButtonProps {
1504
1507
  /**
1505
1508
  * The text content of the confirmation modal.
@@ -1538,6 +1541,11 @@ export interface ButtonProps {
1538
1541
  * The subtitle of the confirmation modal.
1539
1542
  */
1540
1543
  modalSubTitle?: string;
1544
+ /**
1545
+ * The press animation to use when the button is touched.
1546
+ * @default "scale"
1547
+ */
1548
+ pressAnimation?: ButtonPressAnimation;
1541
1549
  /**
1542
1550
  * The test ID for the button, used for testing purposes.
1543
1551
  */
@@ -1675,6 +1683,7 @@ export interface DateTimeActionSheetProps {
1675
1683
  type?: "date" | "time" | "datetime";
1676
1684
  // Returns an ISO 8601 string. If mode is "time", the date portion is today.
1677
1685
  onChange: OnChangeCallback;
1686
+ // biome-ignore lint/suspicious/noExplicitAny: ActionSheet class lives in ActionSheet.tsx which imports from Common.ts; typing this would create a circular import
1678
1687
  actionSheetRef: React.RefObject<any>;
1679
1688
  visible: boolean;
1680
1689
  onDismiss: () => void;
@@ -1686,6 +1695,7 @@ export interface DecimalRangeActionSheetProps {
1686
1695
  min: number;
1687
1696
  max: number;
1688
1697
  onChange: OnChangeCallback;
1698
+ // biome-ignore lint/suspicious/noExplicitAny: ActionSheet class lives in ActionSheet.tsx which imports from Common.ts; typing this would create a circular import
1689
1699
  actionSheetRef: React.RefObject<any>;
1690
1700
  }
1691
1701
 
@@ -1745,6 +1755,7 @@ export type FieldProps =
1745
1755
  export interface HeightActionSheetProps {
1746
1756
  value?: string;
1747
1757
  onChange: OnChangeCallback;
1758
+ // biome-ignore lint/suspicious/noExplicitAny: ActionSheet class lives in ActionSheet.tsx which imports from Common.ts; typing this would create a circular import
1748
1759
  actionSheetRef: React.RefObject<any>;
1749
1760
  /** Minimum height in total inches */
1750
1761
  min?: number;
@@ -1756,14 +1767,17 @@ export interface HeightActionSheetProps {
1756
1767
 
1757
1768
  export interface HyperlinkProps {
1758
1769
  linkDefault?: boolean;
1759
- // noExplicitAny: type comes from external linkify-it library which lacks an exported type
1770
+ // biome-ignore lint/suspicious/noExplicitAny: linkify-it library's main export lacks a TypeScript type definition
1760
1771
  linkify?: any;
1772
+ // biome-ignore lint/suspicious/noExplicitAny: StyleProp's generic is heterogeneous (TextStyle | ViewStyle) for link contexts
1761
1773
  linkStyle?: StyleProp<any>;
1762
1774
  linkText?: string | ((url: string) => string);
1763
1775
  onPress?: (url: string) => void;
1764
1776
  onLongPress?: (url: string, text: string) => void;
1777
+ // biome-ignore lint/suspicious/noExplicitAny: returned view props are spread onto a heterogeneous View; consumers pass arbitrary props
1765
1778
  injectViewProps?: (url: string) => any;
1766
1779
  children?: React.ReactNode;
1780
+ // biome-ignore lint/suspicious/noExplicitAny: StyleProp's generic is heterogeneous for the container which holds mixed Text/View children
1767
1781
  style?: StyleProp<any>;
1768
1782
  }
1769
1783
 
@@ -1913,12 +1927,12 @@ export interface ModalProps {
1913
1927
  /**
1914
1928
  * The function to call when the primary button is clicked.
1915
1929
  */
1916
- // noExplicitAny: callback value type varies by consumer context
1930
+ // biome-ignore lint/suspicious/noExplicitAny: callback value type varies by consumer context
1917
1931
  primaryButtonOnClick?: (value?: any) => void | Promise<void>;
1918
1932
  /**
1919
1933
  * The function to call when the secondary button is clicked.
1920
1934
  */
1921
- // noExplicitAny: callback value type varies by consumer context
1935
+ // biome-ignore lint/suspicious/noExplicitAny: callback value type varies by consumer context
1922
1936
  secondaryButtonOnClick?: (value?: any) => void | Promise<void>;
1923
1937
  }
1924
1938
 
@@ -1927,11 +1941,12 @@ export interface NumberPickerActionSheetProps {
1927
1941
  min: number;
1928
1942
  max: number;
1929
1943
  onChange: OnChangeCallback;
1944
+ // biome-ignore lint/suspicious/noExplicitAny: ActionSheet class lives in ActionSheet.tsx which imports from Common.ts; typing this would create a circular import
1930
1945
  actionSheetRef: React.RefObject<any>;
1931
1946
  }
1932
1947
 
1933
1948
  export interface PageProps {
1934
- // noExplicitAny: React Navigation type varies by navigation stack configuration
1949
+ // biome-ignore lint/suspicious/noExplicitAny: React Navigation type varies by navigation stack configuration
1935
1950
  navigation?: any;
1936
1951
  scroll?: boolean;
1937
1952
  loading?: boolean;
@@ -2252,6 +2267,7 @@ export interface TextFieldPickerActionSheetProps {
2252
2267
  value?: string;
2253
2268
  mode?: "date" | "time";
2254
2269
  onChange: OnChangeCallback;
2270
+ // biome-ignore lint/suspicious/noExplicitAny: ActionSheet class lives in ActionSheet.tsx which imports from Common.ts; typing this would create a circular import
2255
2271
  actionSheetRef: React.RefObject<any>;
2256
2272
  }
2257
2273
 
@@ -2371,19 +2387,19 @@ export type TapToEditProps =
2371
2387
 
2372
2388
  export interface BaseTapToEditProps extends Omit<FieldProps, "onChange" | "value"> {
2373
2389
  title: string;
2374
- // noExplicitAny: value type varies across TapToEdit field types (text, number, date, etc.)
2390
+ // biome-ignore lint/suspicious/noExplicitAny: value type varies across TapToEdit field types (text, number, date, etc.)
2375
2391
  value: any;
2376
2392
 
2377
2393
  /**
2378
2394
  * Not required if not editable.
2379
2395
  */
2380
- // noExplicitAny: value type varies across TapToEdit field types
2396
+ // biome-ignore lint/suspicious/noExplicitAny: value type varies across TapToEdit field types
2381
2397
  setValue?: (value: any) => void;
2382
2398
 
2383
2399
  /**
2384
2400
  * Not required if not editable.
2385
2401
  */
2386
- // noExplicitAny: value type varies across TapToEdit field types
2402
+ // biome-ignore lint/suspicious/noExplicitAny: value type varies across TapToEdit field types
2387
2403
  onSave?: (value: any) => void | Promise<void>;
2388
2404
 
2389
2405
  /**
@@ -2396,7 +2412,7 @@ export interface BaseTapToEditProps extends Omit<FieldProps, "onChange" | "value
2396
2412
  * Enable edit mode from outside the component.
2397
2413
  */
2398
2414
  isEditing?: boolean;
2399
- // noExplicitAny: input value type varies across TapToEdit field types
2415
+ // biome-ignore lint/suspicious/noExplicitAny: input value type varies across TapToEdit field types
2400
2416
  transform?: (value: any) => string;
2401
2417
  /**
2402
2418
  * Show a confirmation modal before saving the value.
@@ -2485,11 +2501,12 @@ export interface ModelFields {
2485
2501
 
2486
2502
  export interface OpenAPISpec {
2487
2503
  paths: {
2488
- // noExplicitAny: OpenAPI path items are deeply accessed with chained property lookups
2504
+ // biome-ignore lint/suspicious/noExplicitAny: OpenAPI path items are deeply accessed with chained property lookups
2489
2505
  [key: string]: any;
2490
2506
  };
2491
2507
  }
2492
2508
 
2509
+ // biome-ignore lint/suspicious/noExplicitAny: ModelFieldConfig is a passthrough for arbitrary field configuration objects from various model contexts
2493
2510
  export type ModelFieldConfig = any;
2494
2511
 
2495
2512
  export interface OpenAPIProviderProps {
@@ -2518,7 +2535,7 @@ export interface ModelAdminFieldConfig {
2518
2535
 
2519
2536
  // The props for a custom column component for ModelAdmin.
2520
2537
  export interface ModelAdminCustomComponentProps extends Omit<FieldProps, "name"> {
2521
- // noExplicitAny: document shape varies by model used with ModelAdmin
2538
+ // biome-ignore lint/suspicious/noExplicitAny: document shape varies by model used with ModelAdmin
2522
2539
  doc: any;
2523
2540
  fieldKey: string; // Dot notation representation of the field.
2524
2541
  // user: User;
@@ -208,6 +208,108 @@ describe("ConsentFormScreen", () => {
208
208
  expect(queryByTestId("consent-form-required-legend")).toBeNull();
209
209
  });
210
210
 
211
+ it("confirms the modal and toggles the checkbox on", () => {
212
+ const form: ConsentFormPublic = {
213
+ ...baseForm,
214
+ checkboxes: [{confirmationPrompt: "Are you sure?", label: "Tricky", required: true}],
215
+ };
216
+ const {getByTestId, getByText, queryByTestId} = renderWithTheme(
217
+ <ConsentFormScreen form={form} locale="en" onAgree={() => {}} />
218
+ );
219
+ act(() => {
220
+ fireEvent.press(getByTestId("consent-form-checkbox-0"));
221
+ });
222
+ expect(getByText("Are you sure?")).toBeTruthy();
223
+ // Press the "Confirm" button inside the modal
224
+ act(() => {
225
+ fireEvent.press(getByText("Confirm"));
226
+ });
227
+ return new Promise<void>((resolve) => {
228
+ setTimeout(() => {
229
+ // Checkbox hint should be gone because the required checkbox was toggled on
230
+ expect(queryByTestId("consent-footer-checkboxes-hint")).toBeNull();
231
+ resolve();
232
+ }, 600);
233
+ });
234
+ });
235
+
236
+ it("dismisses the modal without toggling the checkbox", () => {
237
+ const form: ConsentFormPublic = {
238
+ ...baseForm,
239
+ checkboxes: [{confirmationPrompt: "Are you sure?", label: "Tricky", required: true}],
240
+ };
241
+ const {getByTestId, getByText} = renderWithTheme(
242
+ <ConsentFormScreen form={form} locale="en" onAgree={() => {}} />
243
+ );
244
+ act(() => {
245
+ fireEvent.press(getByTestId("consent-form-checkbox-0"));
246
+ });
247
+ expect(getByText("Are you sure?")).toBeTruthy();
248
+ // Press the "Cancel" button inside the modal
249
+ act(() => {
250
+ fireEvent.press(getByText("Cancel"));
251
+ });
252
+ return new Promise<void>((resolve) => {
253
+ setTimeout(() => {
254
+ // The checkbox hint should still show because the checkbox was not toggled
255
+ expect(getByTestId("consent-footer-checkboxes-hint")).toBeTruthy();
256
+ resolve();
257
+ }, 600);
258
+ });
259
+ });
260
+
261
+ it("auto-satisfies scroll requirement when content fits the viewport via contentSizeChange", () => {
262
+ const form = {...baseForm, requireScrollToBottom: true};
263
+ const {getByTestId, queryByTestId} = renderWithTheme(
264
+ <ConsentFormScreen form={form} locale="en" onAgree={() => {}} />
265
+ );
266
+ const scroll = getByTestId("consent-form-scroll-view");
267
+ // First set the layout height, then content size smaller than layout
268
+ act(() => {
269
+ fireEvent(scroll, "layout", {nativeEvent: {layout: {height: 500}}});
270
+ });
271
+ act(() => {
272
+ fireEvent(scroll, "contentSizeChange", 0, 400);
273
+ });
274
+ expect(queryByTestId("consent-form-scroll-hint")).toBeNull();
275
+ });
276
+
277
+ it("auto-satisfies scroll requirement when content fits the viewport via layout", () => {
278
+ const form = {...baseForm, requireScrollToBottom: true};
279
+ const {getByTestId, queryByTestId} = renderWithTheme(
280
+ <ConsentFormScreen form={form} locale="en" onAgree={() => {}} />
281
+ );
282
+ const scroll = getByTestId("consent-form-scroll-view");
283
+ // First set the content size, then layout height larger than content
284
+ act(() => {
285
+ fireEvent(scroll, "contentSizeChange", 0, 300);
286
+ });
287
+ act(() => {
288
+ fireEvent(scroll, "layout", {nativeEvent: {layout: {height: 500}}});
289
+ });
290
+ expect(queryByTestId("consent-form-scroll-hint")).toBeNull();
291
+ });
292
+
293
+ it("handleScroll returns early when already scrolled to bottom", () => {
294
+ const form = {...baseForm, requireScrollToBottom: false};
295
+ const {getByTestId} = renderWithTheme(
296
+ <ConsentFormScreen form={form} locale="en" onAgree={() => {}} />
297
+ );
298
+ const scroll = getByTestId("consent-form-scroll-view");
299
+ // requireScrollToBottom is false so hasScrolledToBottom starts as true.
300
+ // Firing scroll should hit the early return at line 81.
301
+ act(() => {
302
+ fireEvent(scroll, "scroll", {
303
+ nativeEvent: {
304
+ contentOffset: {y: 0},
305
+ contentSize: {height: 1000},
306
+ layoutMeasurement: {height: 500},
307
+ },
308
+ });
309
+ });
310
+ expect(scroll).toBeTruthy();
311
+ });
312
+
211
313
  it("shows the checkbox footer hint when a required checkbox is unchecked", () => {
212
314
  const form: ConsentFormPublic = {
213
315
  ...baseForm,
@@ -1,5 +1,11 @@
1
1
  import React, {useState} from "react";
2
- import {Pressable, ScrollView} from "react-native";
2
+ import {
3
+ type LayoutChangeEvent,
4
+ type NativeScrollEvent,
5
+ type NativeSyntheticEvent,
6
+ Pressable,
7
+ ScrollView,
8
+ } from "react-native";
3
9
 
4
10
  import {Box} from "./Box";
5
11
  import {Button} from "./Button";
@@ -62,7 +68,7 @@ export const ConsentFormScreen: React.FC<ConsentFormScreenProps> = ({
62
68
  }
63
69
  };
64
70
 
65
- const handleLayout = (event: any) => {
71
+ const handleLayout = (event: LayoutChangeEvent) => {
66
72
  const h = event.nativeEvent.layout.height;
67
73
  setLayoutHeight(h);
68
74
  if (!hasScrolledToBottom && contentHeight > 0 && h > 0 && contentHeight <= h) {
@@ -70,7 +76,7 @@ export const ConsentFormScreen: React.FC<ConsentFormScreenProps> = ({
70
76
  }
71
77
  };
72
78
 
73
- const handleScroll = (event: any) => {
79
+ const handleScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
74
80
  if (hasScrolledToBottom) {
75
81
  return;
76
82
  }
@@ -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 React from "react";
3
4
  import {Pressable} from "react-native";
@@ -10,11 +10,12 @@ import type {SubmitConsentBody} from "./useSubmitConsent";
10
10
  import {useSubmitConsent} from "./useSubmitConsent";
11
11
 
12
12
  interface ConsentNavigatorProps {
13
+ // biome-ignore lint/suspicious/noExplicitAny: RTK Query api instance is a complex generic type that varies per consumer
13
14
  api: any;
14
15
  baseUrl?: string;
15
16
  children: React.ReactNode;
16
17
  extraScreens?: React.ReactNode[];
17
- onError?: (error: any) => void;
18
+ onError?: (error: unknown) => void;
18
19
  variables?: Record<string, string>;
19
20
  }
20
21
 
@@ -48,7 +49,8 @@ export const ConsentNavigator: React.FC<ConsentNavigatorProps> = ({
48
49
  }
49
50
 
50
51
  if (error) {
51
- const status = (error as any)?.status ?? (error as any)?.originalStatus;
52
+ const errorObj = error as {status?: number; originalStatus?: number};
53
+ const status = errorObj?.status ?? errorObj?.originalStatus;
52
54
  console.warn("[ConsentNavigator] Error fetching pending consents:", {error, status});
53
55
  // On auth errors, pass through to let the app handle re-authentication
54
56
  if (status === 401 || status === 403) {
@@ -80,7 +82,7 @@ export const ConsentNavigator: React.FC<ConsentNavigatorProps> = ({
80
82
  `[ConsentNavigator] Showing extra screen ${extraScreenIndex + 1}/${validExtraScreens.length}`
81
83
  );
82
84
  if (React.isValidElement(currentScreen)) {
83
- return React.cloneElement(currentScreen as React.ReactElement<any>, {
85
+ return React.cloneElement(currentScreen as React.ReactElement<{onNext?: () => void}>, {
84
86
  onNext: () => setExtraScreenIndex((i) => i + 1),
85
87
  });
86
88
  }
@@ -101,7 +101,9 @@ export const CustomSelectField: FC<CustomSelectFieldProps> = ({
101
101
  <TextField
102
102
  disabled={disabled}
103
103
  id="customOptions"
104
- inputRef={(ref: any) => (textInputRef.current = ref)}
104
+ inputRef={(ref: TextInput | null) => {
105
+ textInputRef.current = ref;
106
+ }}
105
107
  onChange={onChange}
106
108
  placeholder="None selected"
107
109
  type="text"
@@ -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
 
3
4
  import {DataTable} from "./DataTable";
package/src/DataTable.tsx CHANGED
@@ -264,7 +264,7 @@ const DataTableHeaderCell: FC<DataTableHeaderCellProps> = ({
264
264
  }}
265
265
  >
266
266
  {[
267
- Boolean(column.title) ? (
267
+ column.title ? (
268
268
  <TableTitle align="left" key="data-table-header-title" title={column.title!} />
269
269
  ) : null,
270
270
  <View key="data-table-header-tools" style={{alignItems: "center", flexDirection: "row"}}>
@@ -369,7 +369,11 @@ const DateCalendar = ({
369
369
  const {theme} = useTheme();
370
370
 
371
371
  const markedDates: {
372
- [id: string]: {selected: boolean; selectedColor: string; customStyles?: any};
372
+ [id: string]: {
373
+ selected: boolean;
374
+ selectedColor: string;
375
+ customStyles?: {container?: {backgroundColor?: string; borderRadius?: number}};
376
+ };
373
377
  } = {};
374
378
 
375
379
  // Check if the date is T00:00:00.000Z (it should be), otherwise treat it as a date in the
@@ -471,7 +475,7 @@ export const DateTimeActionSheet = ({
471
475
 
472
476
  // If the value changes in the props, update the state for the date and time.
473
477
  useEffect(() => {
474
- let datetime;
478
+ let datetime: DateTime;
475
479
  if (value) {
476
480
  if (type === "date") {
477
481
  datetime = DateTime.fromISO(value).toUTC().set({millisecond: 0, second: 0});
@@ -495,7 +499,7 @@ export const DateTimeActionSheet = ({
495
499
  setHour(h);
496
500
  setMinute(datetime.minute);
497
501
  setAmPm(datetime.toFormat("a") === "AM" ? "am" : "pm");
498
- setDate(datetime.toISO());
502
+ setDate(datetime.toISO() ?? "");
499
503
  // Reset timezone when the sent date changes.
500
504
  setTimezone(originalTimezone);
501
505
  }, [value, originalTimezone, type]);
@@ -1,3 +1,4 @@
1
+ // biome-ignore-all lint/suspicious/noExplicitAny: test mock typing
1
2
  import {afterEach, beforeEach, describe, expect, it, type mock} from "bun:test";
2
3
  import {act, userEvent} from "@testing-library/react-native";
3
4
  import {DateTime} from "luxon";
@@ -418,6 +418,7 @@ export const DateTimeField: FC<DateTimeFieldProps> = ({
418
418
  helperText,
419
419
  }): React.ReactElement => {
420
420
  const {theme} = useTheme();
421
+ // 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
421
422
  const dateActionSheetRef: React.RefObject<any> = React.createRef();
422
423
  const [amPm, setAmPm] = useState<"am" | "pm">("am");
423
424
  const [showDate, setShowDate] = useState(false);
@@ -567,7 +568,7 @@ export const DateTimeField: FC<DateTimeFieldProps> = ({
567
568
  const dayVal = override?.day ?? day;
568
569
  const yearVal = override?.year ?? year;
569
570
  const hourVal = override?.hour ?? hour;
570
- let date;
571
+ let date: DateTime;
571
572
  if (type === "datetime") {
572
573
  if (!monthVal || !dayVal || !yearVal || !hour || !minuteVal) {
573
574
  return undefined;
@@ -635,7 +636,7 @@ export const DateTimeField: FC<DateTimeFieldProps> = ({
635
636
 
636
637
  if (date.isValid) {
637
638
  // Always return UTC ISO string
638
- return date.toUTC().toISO();
639
+ return date.toUTC().toISO() ?? undefined;
639
640
  }
640
641
  return undefined;
641
642
  },
@@ -1,5 +1,8 @@
1
1
  import {beforeEach, describe, expect, it, mock} from "bun:test";
2
2
  import {
3
+ convertNullToUndefined,
4
+ getIsoDate,
5
+ getTimezoneOptions,
3
6
  humanDate,
4
7
  humanDateAndTime,
5
8
  printDate,
@@ -454,5 +457,113 @@ describe("DateUtilities", () => {
454
457
  // print without ago
455
458
  expect(printSince("2019-12-23T11:00:00.000Z", {showAgo: false})).toBe("3 years");
456
459
  });
460
+
461
+ it("should throw for invalid date", () => {
462
+ expect(() => printSince("not-a-date")).toThrow("printSince: Invalid date: not-a-date");
463
+ });
464
+ });
465
+
466
+ describe("humanDate – non-string input", () => {
467
+ it("should throw for non-string date", () => {
468
+ expect(() => humanDate(123 as unknown as string)).toThrow(
469
+ "humanDate: Invalid date type: number"
470
+ );
471
+ });
472
+ });
473
+
474
+ describe("humanDateAndTime – showTimezone false", () => {
475
+ it("should format time without timezone abbreviation", () => {
476
+ const result = humanDateAndTime("2022-12-24T12:00:00.000Z", {showTimezone: false});
477
+ expect(result).toBe("7:00 AM");
478
+ });
479
+
480
+ it("should format tomorrow without timezone abbreviation", () => {
481
+ const result = humanDateAndTime("2022-12-25T12:00:00.000Z", {showTimezone: false});
482
+ expect(result).toBe("Tomorrow 7:00 AM");
483
+ });
484
+ });
485
+
486
+ describe("printDate – showTimezone warning", () => {
487
+ it("should still return the date when showTimezone is true", () => {
488
+ const result = printDate("2022-12-24T12:00:00.000Z", {showTimezone: true});
489
+ expect(result).toBe("12/24/2022");
490
+ });
491
+ });
492
+
493
+ describe("printDateRange – timeOnly with different dates", () => {
494
+ it("should warn but still return time range when dates differ", () => {
495
+ const result = printDateRange("2022-12-24T12:00:00.000Z", "2022-12-25T18:00:00.000Z", {
496
+ timeOnly: true,
497
+ timezone: "America/New_York",
498
+ });
499
+ expect(result).toBe("7:00 AM - 1:00 PM EST");
500
+ });
501
+ });
502
+
503
+ describe("convertNullToUndefined", () => {
504
+ it("should return the string when given a string", () => {
505
+ expect(convertNullToUndefined("hello")).toBe("hello");
506
+ });
507
+
508
+ it("should return undefined when given null", () => {
509
+ expect(convertNullToUndefined(null)).toBeUndefined();
510
+ });
511
+ });
512
+
513
+ describe("getIsoDate", () => {
514
+ it("should return undefined for undefined input", () => {
515
+ expect(getIsoDate(undefined)).toBeUndefined();
516
+ });
517
+
518
+ it("should return undefined for empty string", () => {
519
+ expect(getIsoDate("")).toBeUndefined();
520
+ });
521
+
522
+ it("should return ISO string for valid date", () => {
523
+ const result = getIsoDate("2022-12-24T12:00:00.000Z");
524
+ expect(result).toBe("2022-12-24T12:00:00.000Z");
525
+ });
526
+ });
527
+
528
+ describe("humanDate – non-Error thrown", () => {
529
+ it("should stringify a non-Error value thrown during date parsing", () => {
530
+ expect(() => humanDate(42 as unknown as string)).toThrow(
531
+ "humanDate: Invalid date type: number"
532
+ );
533
+ });
534
+ });
535
+
536
+ describe("getTimezoneOptions", () => {
537
+ it("returns US timezone options with full labels", () => {
538
+ const options = getTimezoneOptions("USA");
539
+ expect(options.length).toBe(7);
540
+ const labels = options.map((o) => o.label);
541
+ expect(labels).toContain("Eastern");
542
+ expect(labels).toContain("Pacific");
543
+ expect(labels).toContain("Arizona");
544
+ });
545
+
546
+ it("returns US timezone options with short labels", () => {
547
+ const options = getTimezoneOptions("USA", true);
548
+ expect(options.length).toBe(7);
549
+ const azOption = options.find((o) => o.value === "America/Phoenix");
550
+ expect(azOption?.label).toBe("AZ");
551
+ });
552
+
553
+ it("returns worldwide timezone options", () => {
554
+ const options = getTimezoneOptions("Worldwide");
555
+ expect(options.length).toBeGreaterThan(7);
556
+ const values = options.map((o) => o.value);
557
+ expect(values).toContain("America/New_York");
558
+ });
559
+
560
+ it("returns worldwide timezone options with short labels", () => {
561
+ const options = getTimezoneOptions("Worldwide", true);
562
+ expect(options.length).toBeGreaterThan(7);
563
+ options.forEach((o) => {
564
+ expect(typeof o.label).toBe("string");
565
+ expect(typeof o.value).toBe("string");
566
+ });
567
+ });
457
568
  });
458
569
  });
@@ -61,7 +61,7 @@ export function humanDate(
61
61
  date: string,
62
62
  {timezone, dontShowTime}: {timezone?: string; dontShowTime?: boolean} = {}
63
63
  ): string {
64
- let clonedDate;
64
+ let clonedDate: DateTime;
65
65
  try {
66
66
  clonedDate = getDate(date, {timezone});
67
67
  } catch (error: unknown) {
@@ -95,7 +95,7 @@ export function humanDateAndTime(
95
95
  date: string,
96
96
  {timezone, showTimezone = true}: {timezone?: string; showTimezone?: boolean} = {}
97
97
  ): string {
98
- let clonedDate;
98
+ let clonedDate: DateTime;
99
99
  try {
100
100
  clonedDate = getDate(date, {timezone});
101
101
  } catch (error: unknown) {
@@ -162,7 +162,7 @@ export const printDate = (
162
162
  return justDate.startOf("day").toFormat("M/d/yyyy");
163
163
  }
164
164
 
165
- let clonedDate;
165
+ let clonedDate: DateTime;
166
166
  try {
167
167
  clonedDate = getDate(date, {timezone});
168
168
  } catch (error: unknown) {
@@ -214,7 +214,7 @@ export function printTime(
214
214
  if (!date) {
215
215
  return defaultValue ?? "Invalid Date";
216
216
  }
217
- let clonedDate;
217
+ let clonedDate: DateTime;
218
218
  if (!timezone) {
219
219
  throw new Error("printTime: timezone is required");
220
220
  }
@@ -250,7 +250,7 @@ export function printDateAndTime(
250
250
  if (!date) {
251
251
  return defaultValue ?? "Invalid Datetime";
252
252
  }
253
- let clonedDate;
253
+ let clonedDate: DateTime;
254
254
  try {
255
255
  clonedDate = getDate(date, {timezone});
256
256
  } catch (error: unknown) {
@@ -307,7 +307,7 @@ export function printSince(
307
307
  date: string,
308
308
  {timezone, showAgo = true}: {timezone?: string; showAgo?: boolean} = {}
309
309
  ): string {
310
- let clonedDate;
310
+ let clonedDate: DateTime;
311
311
  const ago = showAgo ? " ago" : "";
312
312
  try {
313
313
  clonedDate = getDate(date, {timezone});
@@ -94,4 +94,32 @@ describe("DecimalRangeActionSheet", () => {
94
94
  });
95
95
  expect(handleChange).toHaveBeenCalled();
96
96
  });
97
+
98
+ it("invokes actionSheetRef.setModalVisible(false) when Close is pressed", () => {
99
+ const actionSheetRef = createRef<ActionSheet>();
100
+ const {UNSAFE_getAllByProps} = render(
101
+ <ThemeProvider>
102
+ <DecimalRangeActionSheet
103
+ actionSheetRef={actionSheetRef}
104
+ max={10}
105
+ min={0}
106
+ onChange={() => {}}
107
+ value="5.5"
108
+ />
109
+ </ThemeProvider>
110
+ );
111
+
112
+ // Spy on the ActionSheet instance's setModalVisible after React assigns the ref
113
+ const spy = mock(() => {});
114
+ if (actionSheetRef.current) {
115
+ actionSheetRef.current.setModalVisible = spy;
116
+ }
117
+
118
+ const closeButtons = UNSAFE_getAllByProps({text: "Close"});
119
+ const buttonOnClick = closeButtons[0].props.onClick as () => void;
120
+ act(() => {
121
+ buttonOnClick();
122
+ });
123
+ expect(spy).toHaveBeenCalledWith(false);
124
+ });
97
125
  });