ferns-ui 1.7.0 → 1.8.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 (42) hide show
  1. package/dist/Common.d.ts +42 -0
  2. package/dist/DateTimeField.test.js +5 -9
  3. package/dist/DateTimeField.test.js.map +1 -1
  4. package/dist/Icon.js +2 -1
  5. package/dist/Icon.js.map +1 -1
  6. package/dist/Modal.js +21 -6
  7. package/dist/Modal.js.map +1 -1
  8. package/dist/SelectBadge.d.ts +3 -0
  9. package/dist/SelectBadge.js +180 -0
  10. package/dist/SelectBadge.js.map +1 -0
  11. package/dist/TextArea.test.d.ts +1 -0
  12. package/dist/TextArea.test.js +146 -0
  13. package/dist/TextArea.test.js.map +1 -0
  14. package/dist/TextField.test.d.ts +1 -0
  15. package/dist/TextField.test.js +251 -0
  16. package/dist/TextField.test.js.map +1 -0
  17. package/dist/index.d.ts +1 -0
  18. package/dist/index.js +1 -0
  19. package/dist/index.js.map +1 -1
  20. package/dist/test-utils.d.ts +97 -0
  21. package/dist/test-utils.js +23 -0
  22. package/dist/test-utils.js.map +1 -0
  23. package/dist/useStoredState.d.ts +1 -1
  24. package/dist/useStoredState.js +19 -4
  25. package/dist/useStoredState.js.map +1 -1
  26. package/dist/useStoredState.test.d.ts +1 -0
  27. package/dist/useStoredState.test.js +93 -0
  28. package/dist/useStoredState.test.js.map +1 -0
  29. package/package.json +1 -1
  30. package/src/Common.ts +43 -0
  31. package/src/DateTimeField.test.tsx +5 -10
  32. package/src/Icon.tsx +1 -1
  33. package/src/Modal.tsx +24 -6
  34. package/src/SelectBadge.tsx +279 -0
  35. package/src/TextArea.test.tsx +271 -0
  36. package/src/TextField.test.tsx +442 -0
  37. package/src/__snapshots__/TextArea.test.tsx.snap +424 -0
  38. package/src/__snapshots__/TextField.test.tsx.snap +481 -0
  39. package/src/index.tsx +1 -0
  40. package/src/test-utils.tsx +26 -0
  41. package/src/useStoredState.test.tsx +134 -0
  42. package/src/useStoredState.ts +21 -5
@@ -0,0 +1,481 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`TextField snapshots should match snapshot when disabled 1`] = `
4
+ <View
5
+ style={
6
+ {
7
+ "flexDirection": "column",
8
+ "width": "100%",
9
+ }
10
+ }
11
+ >
12
+ <Text
13
+ style={
14
+ {
15
+ "color": "#1C1C1C",
16
+ "fontSize": 14,
17
+ "fontWeight": 600,
18
+ "lineHeight": 22.4,
19
+ }
20
+ }
21
+ >
22
+ Disabled Field
23
+ </Text>
24
+ <View
25
+ style={
26
+ {
27
+ "alignItems": "center",
28
+ "backgroundColor": "#D9D9D9",
29
+ "borderColor": "#4E4E4E",
30
+ "borderRadius": 4,
31
+ "borderWidth": 1,
32
+ "flexDirection": "row",
33
+ "overflow": "hidden",
34
+ "paddingHorizontal": 12,
35
+ "paddingVertical": 8,
36
+ }
37
+ }
38
+ >
39
+ <TextInput
40
+ accessibilityHint="Enter text here"
41
+ accessibilityState={
42
+ {
43
+ "disabled": true,
44
+ }
45
+ }
46
+ aria-label="Text input field"
47
+ autoCapitalize="sentences"
48
+ autoCorrect={true}
49
+ blurOnSubmit={true}
50
+ keyboardType="default"
51
+ numberOfLines={1}
52
+ onBlur={[Function]}
53
+ onChangeText={[MockFunction]}
54
+ onContentSizeChange={[Function]}
55
+ onFocus={[Function]}
56
+ onSubmitEditing={[Function]}
57
+ placeholderTextColor="#686868"
58
+ readOnly={true}
59
+ secureTextEntry={false}
60
+ style={
61
+ {
62
+ "color": "#1C1C1C",
63
+ "flex": 1,
64
+ "fontFamily": "text",
65
+ "fontSize": 16,
66
+ "gap": 10,
67
+ "height": 20,
68
+ "paddingVertical": 0,
69
+ "width": "100%",
70
+ }
71
+ }
72
+ textContentType="none"
73
+ underlineColorAndroid="transparent"
74
+ value="disabled value"
75
+ />
76
+ </View>
77
+ </View>
78
+ `;
79
+
80
+ exports[`TextField snapshots should match snapshot with all props 1`] = `
81
+ <View
82
+ style={
83
+ {
84
+ "flexDirection": "column",
85
+ "width": "100%",
86
+ }
87
+ }
88
+ >
89
+ <Text
90
+ style={
91
+ {
92
+ "color": "#1C1C1C",
93
+ "fontSize": 14,
94
+ "fontWeight": 600,
95
+ "lineHeight": 22.4,
96
+ }
97
+ }
98
+ >
99
+ Test Title
100
+ </Text>
101
+ <View
102
+ style={
103
+ {
104
+ "alignItems": "center",
105
+ "flexDirection": "row",
106
+ "marginVertical": 2,
107
+ }
108
+ }
109
+ >
110
+ <View
111
+ style={
112
+ {
113
+ "marginLeft": 4,
114
+ }
115
+ }
116
+ >
117
+ <Text
118
+ style={
119
+ {
120
+ "color": "#BD1111",
121
+ "fontSize": 12,
122
+ }
123
+ }
124
+ >
125
+ Error text
126
+ </Text>
127
+ </View>
128
+ </View>
129
+ <View
130
+ style={
131
+ {
132
+ "alignItems": "center",
133
+ "backgroundColor": "#FFFFFF",
134
+ "borderColor": "#D33232",
135
+ "borderRadius": 4,
136
+ "borderWidth": 1,
137
+ "flexDirection": "row",
138
+ "overflow": "hidden",
139
+ "paddingHorizontal": 12,
140
+ "paddingVertical": 8,
141
+ }
142
+ }
143
+ >
144
+ <TextInput
145
+ accessibilityHint="Enter text here"
146
+ accessibilityState={
147
+ {
148
+ "disabled": false,
149
+ }
150
+ }
151
+ aria-label="Text input field"
152
+ autoCapitalize="sentences"
153
+ autoCorrect={true}
154
+ blurOnSubmit={true}
155
+ keyboardType="default"
156
+ multiline={false}
157
+ numberOfLines={1}
158
+ onBlur={[Function]}
159
+ onChangeText={[MockFunction]}
160
+ onContentSizeChange={[Function]}
161
+ onFocus={[Function]}
162
+ onSubmitEditing={[Function]}
163
+ placeholder="Enter text"
164
+ placeholderTextColor="#686868"
165
+ readOnly={false}
166
+ secureTextEntry={false}
167
+ style={
168
+ {
169
+ "color": "#1C1C1C",
170
+ "flex": 1,
171
+ "fontFamily": "text",
172
+ "fontSize": 16,
173
+ "gap": 10,
174
+ "height": 20,
175
+ "paddingVertical": 0,
176
+ "width": "100%",
177
+ }
178
+ }
179
+ textContentType="none"
180
+ underlineColorAndroid="transparent"
181
+ value="test value"
182
+ />
183
+ <View
184
+ accessibilityState={
185
+ {
186
+ "busy": undefined,
187
+ "checked": undefined,
188
+ "disabled": undefined,
189
+ "expanded": undefined,
190
+ "selected": undefined,
191
+ }
192
+ }
193
+ accessibilityValue={
194
+ {
195
+ "max": undefined,
196
+ "min": undefined,
197
+ "now": undefined,
198
+ "text": undefined,
199
+ }
200
+ }
201
+ accessible={true}
202
+ aria-role="button"
203
+ collapsable={false}
204
+ focusable={true}
205
+ onBlur={[Function]}
206
+ onClick={[Function]}
207
+ onFocus={[Function]}
208
+ onResponderGrant={[Function]}
209
+ onResponderMove={[Function]}
210
+ onResponderRelease={[Function]}
211
+ onResponderTerminate={[Function]}
212
+ onResponderTerminationRequest={[Function]}
213
+ onStartShouldSetResponder={[Function]}
214
+ />
215
+ </View>
216
+ <View
217
+ style={
218
+ {
219
+ "marginTop": 2,
220
+ }
221
+ }
222
+ >
223
+ <Text
224
+ style={
225
+ {
226
+ "color": "#1C1C1C",
227
+ "fontSize": 12,
228
+ "lineHeight": 16,
229
+ }
230
+ }
231
+ >
232
+ Helper text
233
+ </Text>
234
+ </View>
235
+ </View>
236
+ `;
237
+
238
+ exports[`TextField snapshots should match snapshot with default props 1`] = `
239
+ <View
240
+ style={
241
+ {
242
+ "flexDirection": "column",
243
+ "width": "100%",
244
+ }
245
+ }
246
+ >
247
+ <View
248
+ style={
249
+ {
250
+ "alignItems": "center",
251
+ "backgroundColor": "#FFFFFF",
252
+ "borderColor": "#9A9A9A",
253
+ "borderRadius": 4,
254
+ "borderWidth": 1,
255
+ "flexDirection": "row",
256
+ "overflow": "hidden",
257
+ "paddingHorizontal": 12,
258
+ "paddingVertical": 8,
259
+ }
260
+ }
261
+ >
262
+ <TextInput
263
+ accessibilityHint="Enter text here"
264
+ accessibilityState={
265
+ {
266
+ "disabled": undefined,
267
+ }
268
+ }
269
+ aria-label="Text input field"
270
+ autoCapitalize="sentences"
271
+ autoCorrect={true}
272
+ blurOnSubmit={true}
273
+ keyboardType="default"
274
+ numberOfLines={1}
275
+ onBlur={[Function]}
276
+ onChangeText={[MockFunction]}
277
+ onContentSizeChange={[Function]}
278
+ onFocus={[Function]}
279
+ onSubmitEditing={[Function]}
280
+ placeholderTextColor="#686868"
281
+ secureTextEntry={false}
282
+ style={
283
+ {
284
+ "color": "#1C1C1C",
285
+ "flex": 1,
286
+ "fontFamily": "text",
287
+ "fontSize": 16,
288
+ "gap": 10,
289
+ "height": 20,
290
+ "paddingVertical": 0,
291
+ "width": "100%",
292
+ }
293
+ }
294
+ textContentType="none"
295
+ underlineColorAndroid="transparent"
296
+ value="test value"
297
+ />
298
+ </View>
299
+ </View>
300
+ `;
301
+
302
+ exports[`TextField snapshots should match snapshot with error state 1`] = `
303
+ <View
304
+ style={
305
+ {
306
+ "flexDirection": "column",
307
+ "width": "100%",
308
+ }
309
+ }
310
+ >
311
+ <Text
312
+ style={
313
+ {
314
+ "color": "#1C1C1C",
315
+ "fontSize": 14,
316
+ "fontWeight": 600,
317
+ "lineHeight": 22.4,
318
+ }
319
+ }
320
+ >
321
+ Error Field
322
+ </Text>
323
+ <View
324
+ style={
325
+ {
326
+ "alignItems": "center",
327
+ "flexDirection": "row",
328
+ "marginVertical": 2,
329
+ }
330
+ }
331
+ >
332
+ <View
333
+ style={
334
+ {
335
+ "marginLeft": 4,
336
+ }
337
+ }
338
+ >
339
+ <Text
340
+ style={
341
+ {
342
+ "color": "#BD1111",
343
+ "fontSize": 12,
344
+ }
345
+ }
346
+ >
347
+ This field is required
348
+ </Text>
349
+ </View>
350
+ </View>
351
+ <View
352
+ style={
353
+ {
354
+ "alignItems": "center",
355
+ "backgroundColor": "#FFFFFF",
356
+ "borderColor": "#D33232",
357
+ "borderRadius": 4,
358
+ "borderWidth": 1,
359
+ "flexDirection": "row",
360
+ "overflow": "hidden",
361
+ "paddingHorizontal": 12,
362
+ "paddingVertical": 8,
363
+ }
364
+ }
365
+ >
366
+ <TextInput
367
+ accessibilityHint="Enter text here"
368
+ accessibilityState={
369
+ {
370
+ "disabled": undefined,
371
+ }
372
+ }
373
+ aria-label="Text input field"
374
+ autoCapitalize="sentences"
375
+ autoCorrect={true}
376
+ blurOnSubmit={true}
377
+ keyboardType="default"
378
+ numberOfLines={1}
379
+ onBlur={[Function]}
380
+ onChangeText={[MockFunction]}
381
+ onContentSizeChange={[Function]}
382
+ onFocus={[Function]}
383
+ onSubmitEditing={[Function]}
384
+ placeholderTextColor="#686868"
385
+ secureTextEntry={false}
386
+ style={
387
+ {
388
+ "color": "#1C1C1C",
389
+ "flex": 1,
390
+ "fontFamily": "text",
391
+ "fontSize": 16,
392
+ "gap": 10,
393
+ "height": 20,
394
+ "paddingVertical": 0,
395
+ "width": "100%",
396
+ }
397
+ }
398
+ textContentType="none"
399
+ underlineColorAndroid="transparent"
400
+ value=""
401
+ />
402
+ </View>
403
+ </View>
404
+ `;
405
+
406
+ exports[`TextField snapshots should match snapshot with multiline 1`] = `
407
+ <View
408
+ style={
409
+ {
410
+ "flexDirection": "column",
411
+ "width": "100%",
412
+ }
413
+ }
414
+ >
415
+ <Text
416
+ style={
417
+ {
418
+ "color": "#1C1C1C",
419
+ "fontSize": 14,
420
+ "fontWeight": 600,
421
+ "lineHeight": 22.4,
422
+ }
423
+ }
424
+ >
425
+ Multiline Field
426
+ </Text>
427
+ <View
428
+ style={
429
+ {
430
+ "alignItems": "center",
431
+ "backgroundColor": "#FFFFFF",
432
+ "borderColor": "#9A9A9A",
433
+ "borderRadius": 4,
434
+ "borderWidth": 1,
435
+ "flexDirection": "row",
436
+ "overflow": "hidden",
437
+ "paddingHorizontal": 12,
438
+ "paddingVertical": 8,
439
+ }
440
+ }
441
+ >
442
+ <TextInput
443
+ accessibilityHint="Enter text here"
444
+ accessibilityState={
445
+ {
446
+ "disabled": undefined,
447
+ }
448
+ }
449
+ aria-label="Text input field"
450
+ autoCapitalize="sentences"
451
+ autoCorrect={true}
452
+ blurOnSubmit={true}
453
+ keyboardType="default"
454
+ multiline={true}
455
+ numberOfLines={3}
456
+ onBlur={[Function]}
457
+ onChangeText={[MockFunction]}
458
+ onContentSizeChange={[Function]}
459
+ onFocus={[Function]}
460
+ onSubmitEditing={[Function]}
461
+ placeholderTextColor="#686868"
462
+ secureTextEntry={false}
463
+ style={
464
+ {
465
+ "color": "#1C1C1C",
466
+ "flex": 1,
467
+ "fontFamily": "text",
468
+ "fontSize": 16,
469
+ "gap": 10,
470
+ "height": 120,
471
+ "paddingVertical": 0,
472
+ "width": "100%",
473
+ }
474
+ }
475
+ textContentType="none"
476
+ underlineColorAndroid="transparent"
477
+ value="line 1\\nline 2"
478
+ />
479
+ </View>
480
+ </View>
481
+ `;
package/src/index.tsx CHANGED
@@ -50,6 +50,7 @@ export * from "./Radio";
50
50
  export * from "./RadioField";
51
51
  export * from "./ScrollView";
52
52
  export * from "./SegmentedControl";
53
+ export * from "./SelectBadge";
53
54
  export * from "./SelectField";
54
55
  export * from "./SideDrawer";
55
56
  export * from "./Signature";
@@ -0,0 +1,26 @@
1
+ import {render} from "@testing-library/react-native";
2
+ import React from "react";
3
+ import {ThemeProvider} from "./Theme";
4
+
5
+ export const renderWithTheme = (ui: React.ReactElement) => {
6
+ return render(<ThemeProvider>{ui}</ThemeProvider>);
7
+ };
8
+
9
+ export const createCommonMocks = () => ({
10
+ onChange: jest.fn(),
11
+ onFocus: jest.fn(),
12
+ onBlur: jest.fn(),
13
+ onEnter: jest.fn(),
14
+ onSubmitEditing: jest.fn(),
15
+ onIconClick: jest.fn(),
16
+ });
17
+
18
+ export const setupComponentTest = () => {
19
+ jest.useFakeTimers();
20
+ return createCommonMocks();
21
+ };
22
+
23
+ export const teardownComponentTest = () => {
24
+ jest.useRealTimers();
25
+ jest.clearAllMocks();
26
+ };
@@ -0,0 +1,134 @@
1
+ import {act, renderHook} from "@testing-library/react-native";
2
+
3
+ import {Unifier} from "./Unifier";
4
+ import {useStoredState} from "./useStoredState";
5
+
6
+ jest.mock("./Unifier", () => ({
7
+ Unifier: {
8
+ storage: {
9
+ getItem: jest.fn(),
10
+ setItem: jest.fn(),
11
+ },
12
+ },
13
+ }));
14
+
15
+ describe("useStoredState", () => {
16
+ beforeEach(() => {
17
+ jest.clearAllMocks();
18
+ });
19
+
20
+ it("should return initialValue and isLoading=true on initial render", async () => {
21
+ (Unifier.storage.getItem as jest.Mock).mockImplementation(
22
+ () => new Promise((resolve) => setTimeout(() => resolve("stored value"), 100))
23
+ );
24
+
25
+ const {result} = renderHook(() => useStoredState("testKey", "initial value"));
26
+
27
+ expect(result.current[0]).toBe("initial value");
28
+ expect(result.current[2]).toBe(true);
29
+
30
+ await act(async () => {
31
+ await new Promise((resolve) => setTimeout(resolve, 200));
32
+ });
33
+
34
+ expect(result.current[0]).toBe("stored value");
35
+ expect(result.current[2]).toBe(false);
36
+ });
37
+
38
+ it("should update state and storage when setter is called", async () => {
39
+ (Unifier.storage.getItem as jest.Mock).mockResolvedValue("stored value");
40
+ (Unifier.storage.setItem as jest.Mock).mockResolvedValue(undefined);
41
+
42
+ const {result} = renderHook(() => useStoredState("testKey", "initial value"));
43
+
44
+ await act(async () => {
45
+ await new Promise((resolve) => setTimeout(resolve, 10));
46
+ });
47
+
48
+ await act(async () => {
49
+ await result.current[1]("new value");
50
+ });
51
+
52
+ expect(result.current[0]).toBe("new value");
53
+
54
+ expect(Unifier.storage.setItem).toHaveBeenCalledWith("testKey", "new value");
55
+ });
56
+
57
+ it("should handle errors when reading from storage", async () => {
58
+ const originalConsoleError = console.error;
59
+ console.error = jest.fn();
60
+
61
+ (Unifier.storage.getItem as jest.Mock).mockRejectedValue(new Error("Storage error"));
62
+
63
+ const {result} = renderHook(() => useStoredState("testKey", "initial value"));
64
+
65
+ await act(async () => {
66
+ await new Promise((resolve) => setTimeout(resolve, 10));
67
+ });
68
+
69
+ expect(result.current[0]).toBe("initial value");
70
+ expect(result.current[2]).toBe(false);
71
+ expect(console.error).toHaveBeenCalled();
72
+
73
+ console.error = originalConsoleError;
74
+ });
75
+
76
+ it("should handle errors when writing to storage", async () => {
77
+ const originalConsoleError = console.error;
78
+ console.error = jest.fn();
79
+
80
+ (Unifier.storage.getItem as jest.Mock).mockResolvedValue("stored value");
81
+ (Unifier.storage.setItem as jest.Mock).mockRejectedValue(new Error("Storage error"));
82
+
83
+ const {result} = renderHook(() => useStoredState("testKey", "initial value"));
84
+
85
+ await act(async () => {
86
+ await new Promise((resolve) => setTimeout(resolve, 10));
87
+ });
88
+
89
+ await act(async () => {
90
+ await result.current[1]("new value");
91
+ });
92
+
93
+ expect(result.current[0]).toBe("stored value");
94
+ expect(console.error).toHaveBeenCalled();
95
+
96
+ console.error = originalConsoleError;
97
+ });
98
+
99
+ it("should handle undefined initialValue", async () => {
100
+ (Unifier.storage.getItem as jest.Mock).mockResolvedValue(null);
101
+
102
+ const {result} = renderHook(() => useStoredState("testKey"));
103
+
104
+ expect(result.current[0]).toBeUndefined();
105
+ expect(result.current[2]).toBe(true);
106
+
107
+ await act(async () => {
108
+ await new Promise((resolve) => setTimeout(resolve, 10));
109
+ });
110
+
111
+ expect(result.current[0]).toBeNull();
112
+ expect(result.current[2]).toBe(false);
113
+ });
114
+
115
+ it("should not update state if component unmounts before storage resolves", async () => {
116
+ (Unifier.storage.getItem as jest.Mock).mockImplementation(
117
+ () => new Promise((resolve) => setTimeout(() => resolve("stored value"), 100))
118
+ );
119
+
120
+ const {result, unmount} = renderHook(() => useStoredState("testKey", "initial value"));
121
+
122
+ expect(result.current[0]).toBe("initial value");
123
+ expect(result.current[2]).toBe(true);
124
+
125
+ unmount();
126
+
127
+ await act(async () => {
128
+ await new Promise((resolve) => setTimeout(resolve, 200));
129
+ });
130
+
131
+ expect(result.current[0]).toBe("initial value");
132
+ expect(result.current[2]).toBe(true);
133
+ });
134
+ });
@@ -1,12 +1,14 @@
1
- import {useCallback, useEffect, useState} from "react";
1
+ import {useCallback, useEffect, useRef, useState} from "react";
2
2
 
3
3
  import {Unifier} from "./Unifier";
4
4
 
5
5
  export const useStoredState = <T>(
6
6
  key: string,
7
7
  initialValue?: T
8
- ): [T | undefined, (value: T | undefined) => Promise<void>] => {
8
+ ): [T | undefined, (value: T | undefined) => Promise<void>, boolean] => {
9
9
  const [state, setState] = useState<T | undefined>(initialValue);
10
+ const [isLoading, setIsLoading] = useState(true);
11
+ const isMounted = useRef(true);
10
12
 
11
13
  // Function to fetch data from AsyncStorage
12
14
  const fetchData = useCallback(async (): Promise<T | undefined> => {
@@ -21,19 +23,33 @@ export const useStoredState = <T>(
21
23
  // Fetch data when the component mounts
22
24
  useEffect(() => {
23
25
  void fetchData().then((value) => {
24
- setState(value);
26
+ if (isMounted.current) {
27
+ setState(value);
28
+ setIsLoading(false);
29
+ }
30
+ }).catch((error) => {
31
+ console.error("Error fetching data:", error);
32
+ if (isMounted.current) {
33
+ setIsLoading(false);
34
+ }
25
35
  });
36
+
37
+ return () => {
38
+ isMounted.current = false;
39
+ };
26
40
  // eslint-disable-next-line react-hooks/exhaustive-deps
27
41
  }, []);
28
42
 
29
43
  const setAsyncStorageState = async (newValue: T | undefined): Promise<void> => {
30
44
  try {
31
45
  await Unifier.storage.setItem(key, newValue);
32
- setState(newValue);
46
+ if (isMounted.current) {
47
+ setState(newValue);
48
+ }
33
49
  } catch (error) {
34
50
  console.error("Error writing data to AsyncStorage:", error);
35
51
  }
36
52
  };
37
53
 
38
- return [state, setAsyncStorageState];
54
+ return [state, setAsyncStorageState, isLoading];
39
55
  };