ferns-ui 2.0.0-beta.2 → 2.0.0-beta.5

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 (149) hide show
  1. package/dist/Accordion.js +7 -2
  2. package/dist/Accordion.js.map +1 -1
  3. package/dist/ActionSheet.js +14 -11
  4. package/dist/ActionSheet.js.map +1 -1
  5. package/dist/AddressField.js +1 -1
  6. package/dist/AddressField.js.map +1 -1
  7. package/dist/Badge.js +1 -1
  8. package/dist/Badge.js.map +1 -1
  9. package/dist/Banner.js +1 -1
  10. package/dist/Banner.js.map +1 -1
  11. package/dist/Box.js +3 -3
  12. package/dist/Box.js.map +1 -1
  13. package/dist/Button.js +0 -1
  14. package/dist/Button.js.map +1 -1
  15. package/dist/CheckBox.js.map +1 -1
  16. package/dist/Common.d.ts +14 -9
  17. package/dist/Common.js.map +1 -1
  18. package/dist/DataTable.js +1 -2
  19. package/dist/DataTable.js.map +1 -1
  20. package/dist/DateTimeField.js +22 -22
  21. package/dist/DateTimeField.js.map +1 -1
  22. package/dist/EmailField.js +17 -37
  23. package/dist/EmailField.js.map +1 -1
  24. package/dist/EmojiSelector.d.ts +114 -0
  25. package/dist/EmojiSelector.js +332 -0
  26. package/dist/EmojiSelector.js.map +1 -0
  27. package/dist/FernsProvider.js +1 -1
  28. package/dist/FernsProvider.js.map +1 -1
  29. package/dist/Heading.js +3 -1
  30. package/dist/Heading.js.map +1 -1
  31. package/dist/Hyperlink.js +1 -1
  32. package/dist/Hyperlink.js.map +1 -1
  33. package/dist/IconButton.js +1 -1
  34. package/dist/IconButton.js.map +1 -1
  35. package/dist/Image.js.map +1 -1
  36. package/dist/MarkdownView.d.ts +5 -0
  37. package/dist/MarkdownView.js +44 -0
  38. package/dist/MarkdownView.js.map +1 -0
  39. package/dist/MobileAddressAutoComplete.js +1 -1
  40. package/dist/MobileAddressAutoComplete.js.map +1 -1
  41. package/dist/Modal.d.ts +1 -1
  42. package/dist/Modal.js +35 -15
  43. package/dist/Modal.js.map +1 -1
  44. package/dist/NumberField.js +10 -4
  45. package/dist/NumberField.js.map +1 -1
  46. package/dist/NumberPickerActionSheet.d.ts +1 -3
  47. package/dist/NumberPickerActionSheet.js +0 -3
  48. package/dist/NumberPickerActionSheet.js.map +1 -1
  49. package/dist/Page.js +1 -1
  50. package/dist/Page.js.map +1 -1
  51. package/dist/Pagination.js +2 -2
  52. package/dist/Pagination.js.map +1 -1
  53. package/dist/Permissions.d.ts +1 -1
  54. package/dist/Permissions.js +2 -2
  55. package/dist/Permissions.js.map +1 -1
  56. package/dist/PickerSelect.js +1 -1
  57. package/dist/PickerSelect.js.map +1 -1
  58. package/dist/SectionDivider.js +1 -1
  59. package/dist/SectionDivider.js.map +1 -1
  60. package/dist/SegmentedControl.js.map +1 -1
  61. package/dist/Signature.native.js +2 -2
  62. package/dist/Signature.native.js.map +1 -1
  63. package/dist/SignatureField.js +2 -2
  64. package/dist/SignatureField.js.map +1 -1
  65. package/dist/Slider.js +3 -3
  66. package/dist/Slider.js.map +1 -1
  67. package/dist/SplitPage.js +7 -7
  68. package/dist/SplitPage.js.map +1 -1
  69. package/dist/SplitPage.native.js +4 -6
  70. package/dist/SplitPage.native.js.map +1 -1
  71. package/dist/TapToEdit.js +3 -3
  72. package/dist/TapToEdit.js.map +1 -1
  73. package/dist/Text.js +1 -1
  74. package/dist/Text.js.map +1 -1
  75. package/dist/TextField.js +9 -2
  76. package/dist/TextField.js.map +1 -1
  77. package/dist/TextFieldNumberActionSheet.d.ts +2 -4
  78. package/dist/TextFieldNumberActionSheet.js +1 -4
  79. package/dist/TextFieldNumberActionSheet.js.map +1 -1
  80. package/dist/Tooltip.js +37 -19
  81. package/dist/Tooltip.js.map +1 -1
  82. package/dist/Unifier.d.ts +0 -1
  83. package/dist/Unifier.js.map +1 -1
  84. package/dist/Utilities.d.ts +1 -1
  85. package/dist/Utilities.js +2 -3
  86. package/dist/Utilities.js.map +1 -1
  87. package/dist/WebAddressAutocomplete.js +2 -1
  88. package/dist/WebAddressAutocomplete.js.map +1 -1
  89. package/dist/index.d.ts +11 -10
  90. package/dist/index.js +11 -9
  91. package/dist/index.js.map +1 -1
  92. package/dist/table/Table.js +14 -15
  93. package/dist/table/Table.js.map +1 -1
  94. package/dist/table/TableHeaderCell.js +2 -2
  95. package/dist/table/TableHeaderCell.js.map +1 -1
  96. package/dist/useStoredState.js +4 -2
  97. package/dist/useStoredState.js.map +1 -1
  98. package/package.json +7 -65
  99. package/src/Accordion.tsx +7 -1
  100. package/src/ActionSheet.tsx +26 -22
  101. package/src/AddressField.tsx +1 -1
  102. package/src/Badge.tsx +1 -1
  103. package/src/Banner.tsx +1 -1
  104. package/src/Box.test.tsx +71 -70
  105. package/src/Box.tsx +21 -9
  106. package/src/Button.tsx +0 -1
  107. package/src/CheckBox.tsx +7 -1
  108. package/src/Common.ts +15 -21
  109. package/src/DataTable.tsx +1 -2
  110. package/src/DateTimeField.tsx +22 -22
  111. package/src/EmailField.tsx +22 -42
  112. package/src/EmojiSelector.test.tsx +61 -0
  113. package/src/EmojiSelector.tsx +510 -0
  114. package/src/FernsProvider.tsx +1 -4
  115. package/src/Heading.tsx +3 -1
  116. package/src/Hyperlink.tsx +1 -1
  117. package/src/IconButton.tsx +2 -2
  118. package/src/Image.tsx +1 -0
  119. package/src/MarkdownView.tsx +67 -0
  120. package/src/MobileAddressAutoComplete.tsx +1 -1
  121. package/src/Modal.tsx +58 -21
  122. package/src/NumberField.tsx +10 -4
  123. package/src/NumberPickerActionSheet.tsx +1 -5
  124. package/src/Page.tsx +1 -1
  125. package/src/Pagination.tsx +2 -11
  126. package/src/Permissions.ts +2 -2
  127. package/src/PickerSelect.tsx +1 -1
  128. package/src/SectionDivider.tsx +1 -1
  129. package/src/SegmentedControl.tsx +3 -1
  130. package/src/Signature.native.tsx +2 -2
  131. package/src/SignatureField.tsx +2 -2
  132. package/src/Slider.tsx +10 -17
  133. package/src/SplitPage.native.tsx +2 -4
  134. package/src/SplitPage.tsx +4 -4
  135. package/src/TapToEdit.tsx +3 -7
  136. package/src/Text.tsx +1 -1
  137. package/src/TextArea.test.tsx +27 -43
  138. package/src/TextField.test.tsx +64 -5
  139. package/src/TextField.tsx +10 -1
  140. package/src/TextFieldNumberActionSheet.tsx +3 -7
  141. package/src/Tooltip.tsx +41 -19
  142. package/src/Unifier.ts +1 -3
  143. package/src/Utilities.tsx +3 -4
  144. package/src/WebAddressAutocomplete.tsx +1 -1
  145. package/src/__snapshots__/EmojiSelector.test.tsx.snap +604 -0
  146. package/src/index.tsx +12 -10
  147. package/src/table/Table.tsx +34 -36
  148. package/src/table/TableHeaderCell.tsx +2 -2
  149. package/src/useStoredState.ts +13 -11
package/src/DataTable.tsx CHANGED
@@ -1,7 +1,6 @@
1
1
  import {FontAwesome6} from "@expo/vector-icons";
2
2
  import React, {FC, useCallback, useMemo, useRef, useState} from "react";
3
3
  import {NativeScrollEvent, NativeSyntheticEvent, Pressable, ScrollView, View} from "react-native";
4
- // @ts-ignore
5
4
  import Markdown from "react-native-markdown-display";
6
5
 
7
6
  import {Box} from "./Box";
@@ -18,9 +17,9 @@ import {Icon} from "./Icon";
18
17
  import {InfoModalIcon} from "./InfoModalIcon";
19
18
  import {Modal} from "./Modal";
20
19
  import {Pagination} from "./Pagination";
21
- import {TableTitle} from "./table/TableTitle";
22
20
  import {Text} from "./Text";
23
21
  import {useTheme} from "./Theme";
22
+ import {TableTitle} from "./table/TableTitle";
24
23
 
25
24
  // TODO: Add permanent horizontal scroll bar so users with only a mouse can scroll left/right
26
25
  // easily.
@@ -262,20 +262,20 @@ export const DateTimeField: FC<DateTimeFieldProps> = ({
262
262
  if (type === "date" || type === "datetime") {
263
263
  if (fieldIndex === 0) {
264
264
  // Month
265
- const monthNum = parseInt(fieldValue);
266
- if (isNaN(monthNum) || monthNum < 1 || monthNum > 12) {
265
+ const monthNum = parseInt(fieldValue, 10);
266
+ if (Number.isNaN(monthNum) || monthNum < 1 || monthNum > 12) {
267
267
  return "Month must be between 1 and 12";
268
268
  }
269
269
  } else if (fieldIndex === 1) {
270
270
  // Day
271
- const dayNum = parseInt(fieldValue);
272
- if (isNaN(dayNum) || dayNum < 1 || dayNum > 31) {
271
+ const dayNum = parseInt(fieldValue, 10);
272
+ if (Number.isNaN(dayNum) || dayNum < 1 || dayNum > 31) {
273
273
  return "Day must be between 1 and 31";
274
274
  }
275
275
  } else if (fieldIndex === 2) {
276
276
  // Year
277
- const yearNum = parseInt(fieldValue);
278
- if (isNaN(yearNum) || yearNum < 1900 || yearNum > 2100) {
277
+ const yearNum = parseInt(fieldValue, 10);
278
+ if (Number.isNaN(yearNum) || yearNum < 1900 || yearNum > 2100) {
279
279
  return "Year must be between 1900 and 2100";
280
280
  }
281
281
  }
@@ -284,14 +284,14 @@ export const DateTimeField: FC<DateTimeFieldProps> = ({
284
284
  if (type === "time" || type === "datetime") {
285
285
  if (fieldIndex === (type === "time" ? 0 : 3)) {
286
286
  // Hour
287
- const hourNum = parseInt(fieldValue);
288
- if (isNaN(hourNum) || hourNum < 1 || hourNum > 12) {
287
+ const hourNum = parseInt(fieldValue, 10);
288
+ if (Number.isNaN(hourNum) || hourNum < 1 || hourNum > 12) {
289
289
  return "Hour must be between 1 and 12";
290
290
  }
291
291
  } else if (fieldIndex === (type === "time" ? 1 : 4)) {
292
292
  // Minute
293
- const minuteNum = parseInt(fieldValue);
294
- if (isNaN(minuteNum) || minuteNum < 0 || minuteNum > 59) {
293
+ const minuteNum = parseInt(fieldValue, 10);
294
+ if (Number.isNaN(minuteNum) || minuteNum < 0 || minuteNum > 59) {
295
295
  return "Minute must be between 0 and 59";
296
296
  }
297
297
  }
@@ -323,7 +323,7 @@ export const DateTimeField: FC<DateTimeFieldProps> = ({
323
323
  if (!monthVal || !dayVal || !yearVal || !hour || !minuteVal) {
324
324
  return undefined;
325
325
  }
326
- let hourNum = parseInt(hourVal);
326
+ let hourNum = parseInt(hourVal, 10);
327
327
  if (ampPmVal === "pm" && hourNum !== 12) {
328
328
  hourNum += 12;
329
329
  } else if (ampPmVal === "am" && hourNum === 12) {
@@ -331,11 +331,11 @@ export const DateTimeField: FC<DateTimeFieldProps> = ({
331
331
  }
332
332
  date = DateTime.fromObject(
333
333
  {
334
- year: parseInt(yearVal),
335
- month: parseInt(monthVal),
336
- day: parseInt(dayVal),
334
+ year: parseInt(yearVal, 10),
335
+ month: parseInt(monthVal, 10),
336
+ day: parseInt(dayVal, 10),
337
337
  hour: hourNum,
338
- minute: parseInt(minuteVal),
338
+ minute: parseInt(minuteVal, 10),
339
339
  second: 0,
340
340
  millisecond: 0,
341
341
  },
@@ -349,9 +349,9 @@ export const DateTimeField: FC<DateTimeFieldProps> = ({
349
349
  }
350
350
  date = DateTime.fromObject(
351
351
  {
352
- year: parseInt(yearVal),
353
- month: parseInt(monthVal),
354
- day: parseInt(dayVal),
352
+ year: parseInt(yearVal, 10),
353
+ month: parseInt(monthVal, 10),
354
+ day: parseInt(dayVal, 10),
355
355
  hour: 0,
356
356
  minute: 0,
357
357
  second: 0,
@@ -365,7 +365,7 @@ export const DateTimeField: FC<DateTimeFieldProps> = ({
365
365
  if (!hour || !minuteVal) {
366
366
  return undefined;
367
367
  }
368
- let hourNum = parseInt(hour);
368
+ let hourNum = parseInt(hour, 10);
369
369
  if (ampPmVal === "pm" && hourNum !== 12) {
370
370
  hourNum += 12;
371
371
  } else if (ampPmVal === "am" && hourNum === 12) {
@@ -374,7 +374,7 @@ export const DateTimeField: FC<DateTimeFieldProps> = ({
374
374
  date = DateTime.fromObject(
375
375
  {
376
376
  hour: hourNum,
377
- minute: parseInt(minuteVal),
377
+ minute: parseInt(minuteVal, 10),
378
378
  second: 0,
379
379
  millisecond: 0,
380
380
  },
@@ -404,7 +404,7 @@ export const DateTimeField: FC<DateTimeFieldProps> = ({
404
404
  // so it's always a valid time and easier to edit.
405
405
  // This lets users freely edit or clear the minute field without breaking the time format.
406
406
  const finalValue = numericValue === "" ? "00" : numericValue.slice(-2);
407
- const minuteNum = parseInt(finalValue);
407
+ const minuteNum = parseInt(finalValue, 10);
408
408
 
409
409
  // Update the minute state so the UI reflects the latest input,
410
410
  // even if it's temporarily invalid
@@ -412,7 +412,7 @@ export const DateTimeField: FC<DateTimeFieldProps> = ({
412
412
  setMinute(finalValue);
413
413
 
414
414
  // Only update ref and result if it's a valid minute value
415
- if (!isNaN(minuteNum) && minuteNum >= 0 && minuteNum <= 59) {
415
+ if (!Number.isNaN(minuteNum) && minuteNum >= 0 && minuteNum <= 59) {
416
416
  pendingValueRef.current = {minute: finalValue};
417
417
  setFieldErrors((prev) => ({...prev, [index]: undefined}));
418
418
 
@@ -13,7 +13,11 @@ export const EmailField: FC<EmailFieldProps> = ({
13
13
  ...rest
14
14
  }) => {
15
15
  const [localValue, setLocalValue] = useState<string>(value || "");
16
- const [error, setError] = useState<string | undefined>(errorText);
16
+
17
+ // Sync local state with incoming prop values
18
+ useEffect(() => {
19
+ setLocalValue(value || "");
20
+ }, [value]);
17
21
 
18
22
  const validateEmail = useCallback((email: string): string | undefined => {
19
23
  if (email.trim() === "") {
@@ -26,60 +30,36 @@ export const EmailField: FC<EmailFieldProps> = ({
26
30
  return undefined;
27
31
  }, []);
28
32
 
29
- // Sync local state with incoming prop values
30
- useEffect(() => {
31
- setLocalValue(value || "");
32
- setError(errorText);
33
- }, [value, errorText]);
34
-
35
- const handleBlur = useCallback(
36
- (email: string) => {
37
- if (onBlur) {
38
- onBlur(email);
39
- }
40
- const validationError = validateEmail(email);
41
- if (validationError) {
42
- setError(validationError);
43
- } else {
44
- setError(undefined);
33
+ const localOnChange = useCallback(
34
+ (e: string) => {
35
+ setLocalValue(e);
36
+ const err = validateEmail(e);
37
+ if (!err && onChange) {
38
+ onChange(e);
45
39
  }
46
40
  },
47
- [onBlur, validateEmail]
41
+ [onChange, validateEmail, setLocalValue]
48
42
  );
49
43
 
50
- const handleChange = useCallback(
51
- (email: string) => {
52
- setLocalValue(email);
53
- const validationError = validateEmail(email);
54
- if (error && !validationError) {
55
- setError(undefined);
56
- }
57
- if (!validationError) {
58
- onChange(email);
44
+ const localOnBlur = useCallback(
45
+ (e: string) => {
46
+ setLocalValue(e);
47
+ const err = validateEmail(e);
48
+ if (!err && onBlur) {
49
+ onBlur(e);
59
50
  }
60
51
  },
61
- [onChange, error, validateEmail]
52
+ [onBlur, validateEmail]
62
53
  );
63
-
64
54
  return (
65
55
  <TextField
66
- errorText={error}
56
+ errorText={errorText || validateEmail(localValue)}
67
57
  iconName={iconName}
68
58
  placeholder={placeholder}
69
59
  type="email"
70
60
  value={localValue}
71
- onBlur={(e) => {
72
- handleBlur(e);
73
- if (onBlur) {
74
- onBlur(value || "");
75
- }
76
- }}
77
- onChange={(e) => {
78
- handleChange(e);
79
- if (onChange) {
80
- onChange;
81
- }
82
- }}
61
+ onBlur={localOnBlur}
62
+ onChange={localOnChange}
83
63
  {...rest}
84
64
  />
85
65
  );
@@ -0,0 +1,61 @@
1
+ import React from "react";
2
+
3
+ import EmojiSelector, {Categories} from "./EmojiSelector";
4
+ import {renderWithTheme} from "./test-utils";
5
+
6
+ describe("EmojiSelector", () => {
7
+ it("renders search bar when showSearchBar is true", () => {
8
+ const {getByPlaceholderText} = renderWithTheme(
9
+ <EmojiSelector
10
+ category={Categories.all}
11
+ columns={6}
12
+ onEmojiSelected={jest.fn()}
13
+ placeholder="Search emojis"
14
+ showHistory={false}
15
+ showSearchBar
16
+ showSectionTitles
17
+ showTabs
18
+ theme="#007AFF"
19
+ />
20
+ );
21
+
22
+ expect(getByPlaceholderText("Search emojis")).toBeTruthy();
23
+ });
24
+
25
+ it("renders tab bar when showTabs is true", () => {
26
+ const {getByText} = renderWithTheme(
27
+ <EmojiSelector
28
+ category={Categories.people}
29
+ columns={6}
30
+ onEmojiSelected={jest.fn()}
31
+ placeholder="Search emojis"
32
+ showHistory={false}
33
+ showSearchBar
34
+ showSectionTitles
35
+ showTabs
36
+ theme="#007AFF"
37
+ />
38
+ );
39
+
40
+ // One of the known category symbols
41
+ expect(getByText("😀")).toBeTruthy();
42
+ });
43
+
44
+ it("matches snapshot", () => {
45
+ const tree = renderWithTheme(
46
+ <EmojiSelector
47
+ category={Categories.people}
48
+ columns={6}
49
+ onEmojiSelected={jest.fn()}
50
+ placeholder="Search emojis"
51
+ showHistory={false}
52
+ showSearchBar
53
+ showSectionTitles
54
+ showTabs
55
+ theme="#007AFF"
56
+ />
57
+ );
58
+
59
+ expect(tree.toJSON()).toMatchSnapshot();
60
+ });
61
+ });