@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.
- package/dist/ActionSheet.d.ts +4 -4
- package/dist/ActionSheet.js.map +1 -1
- package/dist/Avatar.js +1 -1
- package/dist/Avatar.js.map +1 -1
- package/dist/Banner.js.map +1 -1
- package/dist/Box.js +2 -0
- package/dist/Box.js.map +1 -1
- package/dist/Button.d.ts +2 -2
- package/dist/Button.js +35 -23
- package/dist/Button.js.map +1 -1
- package/dist/Common.d.ts +8 -2
- package/dist/Common.js.map +1 -1
- package/dist/ConsentFormScreen.js +1 -1
- package/dist/ConsentFormScreen.js.map +1 -1
- package/dist/ConsentNavigator.d.ts +1 -1
- package/dist/ConsentNavigator.js +2 -1
- package/dist/ConsentNavigator.js.map +1 -1
- package/dist/CustomSelectField.js +3 -1
- package/dist/CustomSelectField.js.map +1 -1
- package/dist/DataTable.js +1 -1
- package/dist/DataTable.js.map +1 -1
- package/dist/DateTimeActionSheet.js +2 -1
- package/dist/DateTimeActionSheet.js.map +1 -1
- package/dist/DateTimeField.js +3 -2
- package/dist/DateTimeField.js.map +1 -1
- package/dist/DateUtilities.js.map +1 -1
- package/dist/HeightField.js.map +1 -1
- package/dist/Hyperlink.js +19 -9
- package/dist/Hyperlink.js.map +1 -1
- package/dist/IconButton.js.map +1 -1
- package/dist/ImageBackground.d.ts +2 -5
- package/dist/ImageBackground.js +1 -1
- package/dist/ImageBackground.js.map +1 -1
- package/dist/ModalSheet.d.ts +3 -2
- package/dist/ModalSheet.js +1 -1
- package/dist/ModalSheet.js.map +1 -1
- package/dist/OfflineBanner.d.ts +21 -0
- package/dist/OfflineBanner.js +25 -0
- package/dist/OfflineBanner.js.map +1 -0
- package/dist/OpenAPIContext.js +1 -1
- package/dist/OpenAPIContext.js.map +1 -1
- package/dist/Page.js +1 -0
- package/dist/Page.js.map +1 -1
- package/dist/Pagination.js.map +1 -1
- package/dist/Permissions.js +3 -0
- package/dist/Permissions.js.map +1 -1
- package/dist/PickerSelect.js +7 -4
- package/dist/PickerSelect.js.map +1 -1
- package/dist/SelectField.js +1 -1
- package/dist/SelectField.js.map +1 -1
- package/dist/SplitPage.js +7 -2
- package/dist/SplitPage.js.map +1 -1
- package/dist/SplitPage.native.js +4 -1
- package/dist/SplitPage.native.js.map +1 -1
- package/dist/TapToEdit.js +10 -11
- package/dist/TapToEdit.js.map +1 -1
- package/dist/Toast.js.map +1 -1
- package/dist/ToastNotifications.js.map +1 -1
- package/dist/Unifier.d.ts +2 -2
- package/dist/Unifier.js +1 -1
- package/dist/Unifier.js.map +1 -1
- package/dist/Utilities.d.ts +8 -4
- package/dist/Utilities.js +1 -1
- package/dist/Utilities.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/ActionSheet.test.tsx +1 -0
- package/src/ActionSheet.tsx +6 -4
- package/src/Avatar.tsx +9 -2
- package/src/Badge.test.tsx +1 -0
- package/src/Banner.tsx +1 -1
- package/src/Box.test.tsx +1 -0
- package/src/Box.tsx +10 -6
- package/src/Button.test.tsx +35 -0
- package/src/Button.tsx +65 -34
- package/src/Common.ts +32 -15
- package/src/ConsentFormScreen.test.tsx +102 -0
- package/src/ConsentFormScreen.tsx +9 -3
- package/src/ConsentNavigator.test.tsx +1 -0
- package/src/ConsentNavigator.tsx +5 -3
- package/src/CustomSelectField.tsx +3 -1
- package/src/DataTable.test.tsx +1 -0
- package/src/DataTable.tsx +1 -1
- package/src/DateTimeActionSheet.tsx +7 -3
- package/src/DateTimeField.test.tsx +1 -0
- package/src/DateTimeField.tsx +3 -2
- package/src/DateUtilities.test.ts +111 -0
- package/src/DateUtilities.tsx +6 -6
- package/src/DecimalRangeActionSheet.test.tsx +28 -0
- package/src/ErrorBoundary.test.tsx +1 -0
- package/src/HeightField.tsx +2 -1
- package/src/Hyperlink.tsx +83 -52
- package/src/IconButton.tsx +1 -1
- package/src/ImageBackground.tsx +5 -6
- package/src/ModalSheet.test.tsx +1 -5
- package/src/ModalSheet.tsx +15 -6
- package/src/NumberField.test.tsx +14 -0
- package/src/OfflineBanner.test.tsx +70 -0
- package/src/OfflineBanner.tsx +54 -0
- package/src/OpenAPIContext.tsx +3 -2
- package/src/Page.tsx +1 -0
- package/src/Pagination.tsx +1 -1
- package/src/Permissions.ts +3 -0
- package/src/PickerSelect.tsx +17 -14
- package/src/SelectBadge.test.tsx +1 -0
- package/src/SelectField.tsx +1 -1
- package/src/Signature.test.tsx +1 -0
- package/src/SplitPage.native.tsx +2 -0
- package/src/SplitPage.tsx +6 -1
- package/src/TapToEdit.test.tsx +17 -0
- package/src/TapToEdit.tsx +11 -11
- package/src/Toast.tsx +1 -1
- package/src/ToastNotifications.tsx +1 -4
- package/src/Tooltip.test.tsx +0 -7
- package/src/Unifier.ts +6 -5
- package/src/Utilities.tsx +9 -6
- package/src/__snapshots__/AddressField.test.tsx.snap +3 -1
- package/src/__snapshots__/Button.test.tsx.snap +92 -50
- package/src/__snapshots__/CustomSelectField.test.tsx.snap +21 -7
- package/src/__snapshots__/DecimalRangeActionSheet.test.tsx.snap +14 -8
- package/src/__snapshots__/ErrorPage.test.tsx.snap +7 -4
- package/src/__snapshots__/Field.test.tsx.snap +18 -6
- package/src/__snapshots__/HeightActionSheet.test.tsx.snap +14 -8
- package/src/__snapshots__/HeightField.test.tsx.snap +35 -20
- package/src/__snapshots__/InfoModalIcon.test.tsx.snap +28 -16
- package/src/__snapshots__/Modal.test.tsx.snap +19 -10
- package/src/__snapshots__/ModalSheet.test.tsx.snap +0 -1
- package/src/__snapshots__/NumberPickerActionSheet.test.tsx.snap +14 -8
- package/src/__snapshots__/Page.test.tsx.snap +7 -4
- package/src/__snapshots__/SelectField.test.tsx.snap +18 -6
- package/src/__snapshots__/TerrenoProvider.test.tsx.snap +0 -2
- package/src/__snapshots__/TimezonePicker.test.tsx.snap +18 -6
- package/src/bunSetup.ts +25 -2
- package/src/index.tsx +1 -0
- package/src/login/__snapshots__/LoginScreen.test.tsx.snap +15 -6
- package/src/signUp/__snapshots__/SignUpScreen.test.tsx.snap +10 -4
- package/src/table/__snapshots__/TableBadge.test.tsx.snap +3 -1
- 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<
|
|
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:
|
|
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 {
|
|
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:
|
|
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:
|
|
79
|
+
const handleScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
74
80
|
if (hasScrolledToBottom) {
|
|
75
81
|
return;
|
|
76
82
|
}
|
package/src/ConsentNavigator.tsx
CHANGED
|
@@ -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:
|
|
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
|
|
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<
|
|
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:
|
|
104
|
+
inputRef={(ref: TextInput | null) => {
|
|
105
|
+
textInputRef.current = ref;
|
|
106
|
+
}}
|
|
105
107
|
onChange={onChange}
|
|
106
108
|
placeholder="None selected"
|
|
107
109
|
type="text"
|
package/src/DataTable.test.tsx
CHANGED
package/src/DataTable.tsx
CHANGED
|
@@ -264,7 +264,7 @@ const DataTableHeaderCell: FC<DataTableHeaderCellProps> = ({
|
|
|
264
264
|
}}
|
|
265
265
|
>
|
|
266
266
|
{[
|
|
267
|
-
|
|
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]: {
|
|
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]);
|
package/src/DateTimeField.tsx
CHANGED
|
@@ -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
|
});
|
package/src/DateUtilities.tsx
CHANGED
|
@@ -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
|
});
|