@terreno/ui 0.14.2 → 0.15.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 (43) hide show
  1. package/dist/Badge.js +1 -0
  2. package/dist/Badge.js.map +1 -1
  3. package/dist/Banner.d.ts +8 -0
  4. package/dist/Banner.js +2 -2
  5. package/dist/Banner.js.map +1 -1
  6. package/dist/PickerSelect.js +6 -2
  7. package/dist/PickerSelect.js.map +1 -1
  8. package/dist/Signature.d.ts +8 -1
  9. package/dist/Signature.js +93 -18
  10. package/dist/Signature.js.map +1 -1
  11. package/dist/Signature.native.d.ts +15 -0
  12. package/dist/Signature.native.js +116 -21
  13. package/dist/Signature.native.js.map +1 -1
  14. package/dist/TapToEdit.js +1 -1
  15. package/dist/TapToEdit.js.map +1 -1
  16. package/dist/index.d.ts +1 -1
  17. package/dist/index.js +1 -1
  18. package/dist/index.js.map +1 -1
  19. package/package.json +2 -4
  20. package/src/Badge.test.tsx +7 -0
  21. package/src/Badge.tsx +1 -0
  22. package/src/Banner.test.tsx +23 -3
  23. package/src/Banner.tsx +3 -3
  24. package/src/DateTimeField.test.tsx +226 -0
  25. package/src/Field.test.tsx +23 -0
  26. package/src/PickerSelect.test.tsx +22 -0
  27. package/src/PickerSelect.tsx +24 -8
  28. package/src/Signature.native.tsx +147 -30
  29. package/src/Signature.test.tsx +2 -49
  30. package/src/Signature.tsx +128 -22
  31. package/src/SignatureField.test.tsx +0 -9
  32. package/src/TapToEdit.test.tsx +33 -0
  33. package/src/TapToEdit.tsx +1 -1
  34. package/src/ToastNotifications.test.tsx +74 -1
  35. package/src/__snapshots__/CustomSelectField.test.tsx.snap +5 -4
  36. package/src/__snapshots__/Field.test.tsx.snap +377 -0
  37. package/src/__snapshots__/PickerSelect.test.tsx.snap +5 -4
  38. package/src/__snapshots__/SegmentedControl.test.tsx.snap +9 -0
  39. package/src/__snapshots__/SelectField.test.tsx.snap +5 -4
  40. package/src/__snapshots__/Signature.test.tsx.snap +13 -3
  41. package/src/__snapshots__/SignatureField.test.tsx.snap +10 -3
  42. package/src/bunSetup.ts +0 -15
  43. package/src/index.tsx +1 -1
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -26,6 +26,7 @@
26
26
  "@react-native-community/slider": "5.0.1",
27
27
  "@react-native-picker/picker": "2.11.4",
28
28
  "@react-navigation/native": "^7.1.8",
29
+ "@shopify/react-native-skia": "2.2.12",
29
30
  "emoji-datasource": "^16.0.0",
30
31
  "expo-clipboard": "~8.0.8",
31
32
  "expo-document-picker": "~14.0.8",
@@ -67,12 +68,10 @@
67
68
  "react-native-reanimated": "~4.1.1",
68
69
  "react-native-safe-area-context": "~5.6.0",
69
70
  "react-native-screens": "~4.16.0",
70
- "react-native-signature-canvas": "^4.7.4",
71
71
  "react-native-svg": "15.12.1",
72
72
  "react-native-swiper-flatlist": "^3.2.5",
73
73
  "react-native-web": "~0.21.0",
74
74
  "react-native-webview": "13.15.0",
75
- "react-signature-canvas": "^1.1.0-alpha.2",
76
75
  "react-time-picker": "^8.0.2"
77
76
  },
78
77
  "devDependencies": {
@@ -87,7 +86,6 @@
87
86
  "@types/minimatch": "^6.0.0",
88
87
  "@types/react": "~19.1.10",
89
88
  "@types/react-datetime-picker": "^5.0.0",
90
- "@types/react-signature-canvas": "^1.0.5",
91
89
  "@types/react-time-picker": "^6.0.0",
92
90
  "babel-preset-react-app": "^10.0.1",
93
91
  "expo-modules-core": "~3.0.29",
@@ -138,5 +136,5 @@
138
136
  "test:coverage": "TZ=America/New_York bun run ../scripts/check-coverage.ts",
139
137
  "types": "bun typedoc"
140
138
  },
141
- "version": "0.14.2"
139
+ "version": "0.15.0"
142
140
  }
@@ -34,6 +34,13 @@ describe("Badge", () => {
34
34
  expect(getByText("50")).toBeTruthy();
35
35
  });
36
36
 
37
+ it("keeps number badges scannable with a minimum width", () => {
38
+ const {getByTestId} = renderWithTheme(
39
+ <Badge testID="number-badge" value={5} variant="numberOnly" />
40
+ );
41
+ expect(getByTestId("number-badge")).toHaveStyle({minWidth: 20});
42
+ });
43
+
37
44
  it("applies correct status colors", () => {
38
45
  const statuses = ["error", "warning", "info", "success", "neutral"] as const;
39
46
 
package/src/Badge.tsx CHANGED
@@ -108,6 +108,7 @@ export const Badge = ({
108
108
  paddingVertical: variant === "iconOnly" ? 1 : theme.spacing.xs,
109
109
  width: variant === "iconOnly" ? 16 : "auto",
110
110
  },
111
+ ...(variant === "numberOnly" ? [{minWidth: 20}] : []),
111
112
  isIconOnly && {height: 16, width: 16},
112
113
  secondary && {borderColor, borderWidth: 1},
113
114
  ]}
@@ -1,12 +1,17 @@
1
- import {describe, expect, it, mock} from "bun:test";
2
- import {act, fireEvent, waitFor} from "@testing-library/react-native";
1
+ import {beforeEach, describe, expect, it, mock} from "bun:test";
2
+ import {act, fireEvent, render, waitFor} from "@testing-library/react-native";
3
3
  import React from "react";
4
4
 
5
- import {Banner, hideBanner} from "./Banner";
5
+ import {Banner, BannerButton, hideBanner} from "./Banner";
6
+ import {ThemeContext} from "./Theme";
6
7
  import {renderWithTheme} from "./test-utils";
7
8
  import {Unifier} from "./Unifier";
8
9
 
9
10
  describe("Banner", () => {
11
+ beforeEach(() => {
12
+ Unifier.storage.getItem = mock(() => Promise.resolve(null));
13
+ Unifier.storage.setItem = mock(() => Promise.resolve());
14
+ });
10
15
  it("renders correctly with default props", () => {
11
16
  const {toJSON} = renderWithTheme(<Banner id="test-banner" text="Test message" />);
12
17
  expect(toJSON()).toMatchSnapshot();
@@ -274,4 +279,19 @@ describe("Banner", () => {
274
279
  expect(queryByText("Non persistent")).toBeNull();
275
280
  });
276
281
  });
282
+
283
+ it("BannerButton returns null when theme is not available", () => {
284
+ const nullThemeContext = {
285
+ resetTheme: () => {},
286
+ setPrimitives: () => {},
287
+ setTheme: () => {},
288
+ theme: null,
289
+ };
290
+ const {toJSON} = render(
291
+ <ThemeContext.Provider value={nullThemeContext as never}>
292
+ <BannerButton buttonOnClick={() => {}} buttonText="Test" />
293
+ </ThemeContext.Provider>
294
+ );
295
+ expect(toJSON()).toBeNull();
296
+ });
277
297
  });
package/src/Banner.tsx CHANGED
@@ -17,7 +17,7 @@ type BannerButtonProps = {
17
17
  loading?: boolean;
18
18
  };
19
19
 
20
- const BannerButton = ({
20
+ export const BannerButton = ({
21
21
  loading: propsLoading,
22
22
  buttonText,
23
23
  buttonIconName,
@@ -178,14 +178,14 @@ export const Banner = (props: BannerProps): React.ReactElement | null => {
178
178
  <View style={{paddingLeft: 16, paddingRight: 10}}>
179
179
  <BannerButton
180
180
  buttonIconName={buttonIconName}
181
- buttonOnClick={buttonOnClick ?? (() => {})}
181
+ buttonOnClick={buttonOnClick!}
182
182
  buttonText={buttonText}
183
183
  />
184
184
  </View>
185
185
  )}
186
186
  {Boolean(buttonText && !buttonIconName && buttonOnClick) && (
187
187
  <View style={{paddingLeft: 16, paddingRight: 10}}>
188
- <BannerButton buttonOnClick={buttonOnClick ?? (() => {})} buttonText={buttonText} />
188
+ <BannerButton buttonOnClick={buttonOnClick!} buttonText={buttonText} />
189
189
  </View>
190
190
  )}
191
191
  </View>
@@ -1105,4 +1105,230 @@ describe("DateTimeField", () => {
1105
1105
  expect(mockOnChange).toHaveBeenCalled();
1106
1106
  });
1107
1107
  });
1108
+
1109
+ describe("12 AM handling in time type (getISOFromFields)", () => {
1110
+ it("should convert hour 12 AM to 0 in time type", async () => {
1111
+ setDesktop();
1112
+ const user = userEvent.setup();
1113
+ // 04:00 UTC = 00:00 (12:00 AM) in America/New_York
1114
+ const {getByPlaceholderText} = renderWithTheme(
1115
+ <DateTimeField
1116
+ onChange={mockOnChange}
1117
+ timezone="America/New_York"
1118
+ type="time"
1119
+ value="2023-05-15T04:00:00.000Z"
1120
+ />
1121
+ );
1122
+ expect(getByPlaceholderText("hh").props.value).toBe("12");
1123
+
1124
+ const minuteInput = getByPlaceholderText("mm");
1125
+ await user.clear(minuteInput);
1126
+ await user.type(minuteInput, "15");
1127
+ await act(async () => {
1128
+ await new Promise((resolve) => setTimeout(resolve, 100));
1129
+ });
1130
+ expect(mockOnChange).toHaveBeenCalled();
1131
+ const lastCall = mockOnChange.mock.calls[mockOnChange.mock.calls.length - 1][0];
1132
+ const parsed = DateTime.fromISO(lastCall).setZone("America/New_York");
1133
+ expect(parsed.hour).toBe(0);
1134
+ expect(parsed.minute).toBe(15);
1135
+ });
1136
+
1137
+ it("should convert hour 12 AM to 0 in datetime type", async () => {
1138
+ setDesktop();
1139
+ const user = userEvent.setup();
1140
+ // 04:30 UTC = 00:30 (12:30 AM) in America/New_York
1141
+ const {getByPlaceholderText} = renderWithTheme(
1142
+ <DateTimeField
1143
+ onChange={mockOnChange}
1144
+ timezone="America/New_York"
1145
+ type="datetime"
1146
+ value="2023-05-15T04:30:00.000Z"
1147
+ />
1148
+ );
1149
+ expect(getByPlaceholderText("hh").props.value).toBe("12");
1150
+
1151
+ const minuteInput = getByPlaceholderText("mm");
1152
+ await user.clear(minuteInput);
1153
+ await user.type(minuteInput, "45");
1154
+ await act(async () => {
1155
+ await new Promise((resolve) => setTimeout(resolve, 100));
1156
+ });
1157
+ expect(mockOnChange).toHaveBeenCalled();
1158
+ const lastCall = mockOnChange.mock.calls[mockOnChange.mock.calls.length - 1][0];
1159
+ const parsed = DateTime.fromISO(lastCall).setZone("America/New_York");
1160
+ expect(parsed.hour).toBe(0);
1161
+ });
1162
+ });
1163
+
1164
+ describe("onActionSheetChange invalid date handling", () => {
1165
+ it("should warn and return early for invalid ISO string", async () => {
1166
+ setDesktop();
1167
+ const warnSpy = mock(() => {});
1168
+ const originalWarn = console.warn;
1169
+ console.warn = warnSpy;
1170
+
1171
+ const {UNSAFE_root} = renderWithTheme(
1172
+ <DateTimeField
1173
+ onChange={mockOnChange}
1174
+ timezone="America/New_York"
1175
+ type="date"
1176
+ value="2023-05-15T00:00:00.000Z"
1177
+ />
1178
+ );
1179
+
1180
+ mockOnChange.mockClear();
1181
+ const actionSheet = UNSAFE_root.findAll(
1182
+ (n: any) => n.props?.onChange && n.props?.visible !== undefined
1183
+ );
1184
+ expect(actionSheet.length).toBeGreaterThan(0);
1185
+ await act(async () => {
1186
+ actionSheet[0].props.onChange("not-a-valid-date");
1187
+ });
1188
+ expect(warnSpy).toHaveBeenCalledWith(
1189
+ "Invalid date passed to DateTimeField",
1190
+ "not-a-valid-date"
1191
+ );
1192
+ expect(mockOnChange).not.toHaveBeenCalled();
1193
+
1194
+ console.warn = originalWarn;
1195
+ });
1196
+ });
1197
+
1198
+ describe("useEffect invalid value handling", () => {
1199
+ it("should warn and return early for invalid non-empty value prop", () => {
1200
+ const warnSpy = mock(() => {});
1201
+ const originalWarn = console.warn;
1202
+ console.warn = warnSpy;
1203
+
1204
+ const {getByPlaceholderText} = renderWithTheme(
1205
+ <DateTimeField onChange={mockOnChange} type="date" value="invalid-date-string" />
1206
+ );
1207
+
1208
+ expect(warnSpy).toHaveBeenCalledWith(
1209
+ "Invalid date passed to DateTimeField",
1210
+ "invalid-date-string"
1211
+ );
1212
+ expect(getByPlaceholderText("MM").props.value).toBe("");
1213
+
1214
+ console.warn = originalWarn;
1215
+ });
1216
+
1217
+ it("should warn for invalid value in time type", () => {
1218
+ const warnSpy = mock(() => {});
1219
+ const originalWarn = console.warn;
1220
+ console.warn = warnSpy;
1221
+
1222
+ renderWithTheme(
1223
+ <DateTimeField
1224
+ onChange={mockOnChange}
1225
+ timezone="America/New_York"
1226
+ type="time"
1227
+ value="not-valid"
1228
+ />
1229
+ );
1230
+
1231
+ expect(warnSpy).toHaveBeenCalledWith("Invalid date passed to DateTimeField", "not-valid");
1232
+
1233
+ console.warn = originalWarn;
1234
+ });
1235
+ });
1236
+
1237
+ describe("getFieldValue datetime hour/minute indices", () => {
1238
+ it("should return hour and minute for datetime indices 3 and 4", () => {
1239
+ setDesktop();
1240
+ // 20:30 UTC = 4:30 PM in America/New_York
1241
+ const {getByPlaceholderText} = renderWithTheme(
1242
+ <DateTimeField
1243
+ onChange={mockOnChange}
1244
+ timezone="America/New_York"
1245
+ type="datetime"
1246
+ value="2023-05-15T20:30:00.000Z"
1247
+ />
1248
+ );
1249
+ // Indices 0-2 are date fields, indices 3-4 are hour/minute
1250
+ expect(getByPlaceholderText("hh").props.value).toBe("04");
1251
+ expect(getByPlaceholderText("mm").props.value).toBe("30");
1252
+ });
1253
+
1254
+ it("should return hour and minute for datetime at midnight", () => {
1255
+ setDesktop();
1256
+ // 04:00 UTC = 00:00 (12:00 AM) in America/New_York
1257
+ const {getByPlaceholderText} = renderWithTheme(
1258
+ <DateTimeField
1259
+ onChange={mockOnChange}
1260
+ timezone="America/New_York"
1261
+ type="datetime"
1262
+ value="2023-05-15T04:00:00.000Z"
1263
+ />
1264
+ );
1265
+ expect(getByPlaceholderText("hh").props.value).toBe("12");
1266
+ expect(getByPlaceholderText("mm").props.value).toBe("00");
1267
+ });
1268
+ });
1269
+
1270
+ describe("handleTimezoneChange branches", () => {
1271
+ it("should call onTimezoneChange when provided for datetime type", async () => {
1272
+ setDesktop();
1273
+ const mockTzChange = mock(() => {});
1274
+ const {UNSAFE_root} = renderWithTheme(
1275
+ <DateTimeField
1276
+ onChange={mockOnChange}
1277
+ onTimezoneChange={mockTzChange}
1278
+ timezone="America/New_York"
1279
+ type="datetime"
1280
+ value="2023-05-15T15:30:00.000Z"
1281
+ />
1282
+ );
1283
+
1284
+ const tzPickers = UNSAFE_root.findAll((n: any) => n.props?.onTimezoneChange);
1285
+ expect(tzPickers.length).toBeGreaterThan(0);
1286
+ await act(async () => {
1287
+ tzPickers[0].props.onTimezoneChange("America/Chicago");
1288
+ });
1289
+ expect(mockTzChange).toHaveBeenCalledWith("America/Chicago");
1290
+ });
1291
+
1292
+ it("should set local timezone when onTimezoneChange not provided for datetime type", async () => {
1293
+ setDesktop();
1294
+ const {UNSAFE_root} = renderWithTheme(
1295
+ <DateTimeField
1296
+ onChange={mockOnChange}
1297
+ timezone="America/New_York"
1298
+ type="datetime"
1299
+ value="2023-05-15T15:30:00.000Z"
1300
+ />
1301
+ );
1302
+
1303
+ const tzPickers = UNSAFE_root.findAll((n: any) => n.props?.onTimezoneChange);
1304
+ expect(tzPickers.length).toBeGreaterThan(0);
1305
+ await act(async () => {
1306
+ tzPickers[0].props.onTimezoneChange("America/Chicago");
1307
+ });
1308
+ expect(mockOnChange).toHaveBeenCalled();
1309
+ });
1310
+ });
1311
+
1312
+ describe("minute validation in validateField", () => {
1313
+ it("should validate minute field for datetime type via hour change triggering revalidation", async () => {
1314
+ setDesktop();
1315
+ const user = userEvent.setup();
1316
+ const {getByPlaceholderText} = renderWithTheme(
1317
+ <DateTimeField
1318
+ onChange={mockOnChange}
1319
+ timezone="America/New_York"
1320
+ type="datetime"
1321
+ value="2023-05-15T15:30:00.000Z"
1322
+ />
1323
+ );
1324
+ // Type an invalid hour (triggers validateField for datetime index 3)
1325
+ const hourInput = getByPlaceholderText("hh");
1326
+ await user.clear(hourInput);
1327
+ await user.type(hourInput, "0");
1328
+ await act(async () => {
1329
+ await new Promise((resolve) => setTimeout(resolve, 100));
1330
+ });
1331
+ expect(hourInput).toBeTruthy();
1332
+ });
1333
+ });
1108
1334
  });
@@ -125,4 +125,27 @@ describe("Field", () => {
125
125
  );
126
126
  expect(toJSON()).toMatchSnapshot();
127
127
  });
128
+
129
+ it("renders customSelect field", () => {
130
+ const {toJSON} = renderWithTheme(
131
+ <Field
132
+ label="Custom"
133
+ onChange={() => {}}
134
+ options={[
135
+ {label: "Option A", value: "a"},
136
+ {label: "Option B", value: "b"},
137
+ ]}
138
+ type="customSelect"
139
+ value="a"
140
+ />
141
+ );
142
+ expect(toJSON()).toMatchSnapshot();
143
+ });
144
+
145
+ it("renders signature field", () => {
146
+ const {toJSON} = renderWithTheme(
147
+ <Field label="Sign here" onChange={() => {}} type="signature" value="" />
148
+ );
149
+ expect(toJSON()).toMatchSnapshot();
150
+ });
128
151
  });
@@ -308,6 +308,28 @@ describe("PickerSelect", () => {
308
308
  restoreDocument();
309
309
  }
310
310
  });
311
+
312
+ it("calls onValueChange when a web dropdown option is selected", async () => {
313
+ ensureDocument();
314
+ savedOS = PlatformModule.OS;
315
+ try {
316
+ PlatformModule.OS = "web";
317
+ const mockOnValueChange = mock(() => {});
318
+ const {getByTestId} = renderWithTheme(
319
+ <RNPickerSelect {...defaultProps} onValueChange={mockOnValueChange} value="1" />
320
+ );
321
+ await act(async () => {
322
+ fireEvent.press(getByTestId("web_picker"));
323
+ });
324
+ await act(async () => {
325
+ fireEvent.press(getByTestId("web_dropdown_option_2"));
326
+ });
327
+ expect(mockOnValueChange).toHaveBeenCalledWith("2", 2);
328
+ } finally {
329
+ PlatformModule.OS = savedOS;
330
+ restoreDocument();
331
+ }
332
+ });
311
333
  });
312
334
 
313
335
  describe("android rendering", () => {
@@ -38,6 +38,7 @@ import {
38
38
  Text,
39
39
  TextInput,
40
40
  type TextInputProps,
41
+ type TextProps,
41
42
  TouchableOpacity,
42
43
  View,
43
44
  } from "react-native";
@@ -388,6 +389,7 @@ export const RNPickerSelect = ({
388
389
  return <View style={{pointerEvents: "box-only"}}>{children}</View>;
389
390
  }
390
391
 
392
+ const textProps = textInputProps as Partial<TextProps> | undefined;
391
393
  return (
392
394
  <View
393
395
  style={{
@@ -397,13 +399,27 @@ export const RNPickerSelect = ({
397
399
  width: "100%",
398
400
  }}
399
401
  >
400
- <TextInput
401
- readOnly
402
- style={{color: disabled ? theme.text.secondaryLight : theme.text.primary}}
403
- testID="text_input"
404
- value={selectedItem?.inputLabel ? selectedItem?.inputLabel : selectedItem?.label}
405
- {...textInputProps}
406
- />
402
+ {disabled ? (
403
+ <Text
404
+ {...textProps}
405
+ style={
406
+ textProps?.style
407
+ ? [{color: theme.text.secondaryLight, flex: 1}, textProps.style]
408
+ : {color: theme.text.secondaryLight, flex: 1}
409
+ }
410
+ testID={textInputProps?.testID ?? "text_input"}
411
+ >
412
+ {selectedItem?.inputLabel ? selectedItem?.inputLabel : selectedItem?.label}
413
+ </Text>
414
+ ) : (
415
+ <TextInput
416
+ readOnly
417
+ style={{color: theme.text.primary}}
418
+ testID="text_input"
419
+ value={selectedItem?.inputLabel ? selectedItem?.inputLabel : selectedItem?.label}
420
+ {...textInputProps}
421
+ />
422
+ )}
407
423
  {renderIcon()}
408
424
  </View>
409
425
  );
@@ -629,7 +645,7 @@ export const RNPickerSelect = ({
629
645
  {...touchableWrapperProps}
630
646
  >
631
647
  <Text
632
- numberOfLines={1}
648
+ numberOfLines={disabled ? undefined : 1}
633
649
  style={{
634
650
  color: disabled ? theme.text.secondaryLight : theme.text.primary,
635
651
  flex: 1,