@terreno/ui 0.7.2 → 0.8.1

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 (82) hide show
  1. package/dist/BooleanField.js +23 -23
  2. package/dist/BooleanField.js.map +1 -1
  3. package/dist/ConsentFormScreen.d.ts +14 -0
  4. package/dist/ConsentFormScreen.js +93 -0
  5. package/dist/ConsentFormScreen.js.map +1 -0
  6. package/dist/ConsentHistory.d.ts +8 -0
  7. package/dist/ConsentHistory.js +70 -0
  8. package/dist/ConsentHistory.js.map +1 -0
  9. package/dist/ConsentNavigator.d.ts +9 -0
  10. package/dist/ConsentNavigator.js +72 -0
  11. package/dist/ConsentNavigator.js.map +1 -0
  12. package/dist/DataTable.js +1 -1
  13. package/dist/DataTable.js.map +1 -1
  14. package/dist/DateTimeActionSheet.js +22 -6
  15. package/dist/DateTimeActionSheet.js.map +1 -1
  16. package/dist/DateTimeField.d.ts +22 -0
  17. package/dist/DateTimeField.js +187 -67
  18. package/dist/DateTimeField.js.map +1 -1
  19. package/dist/DraggableList.d.ts +66 -0
  20. package/dist/DraggableList.js +241 -0
  21. package/dist/DraggableList.js.map +1 -0
  22. package/dist/Link.js +1 -1
  23. package/dist/Link.js.map +1 -1
  24. package/dist/MarkdownEditor.d.ts +12 -0
  25. package/dist/MarkdownEditor.js +12 -0
  26. package/dist/MarkdownEditor.js.map +1 -0
  27. package/dist/MarkdownEditorField.d.ts +1 -0
  28. package/dist/MarkdownEditorField.js +16 -16
  29. package/dist/MarkdownEditorField.js.map +1 -1
  30. package/dist/Modal.js +11 -1
  31. package/dist/Modal.js.map +1 -1
  32. package/dist/PickerSelect.js +10 -0
  33. package/dist/PickerSelect.js.map +1 -1
  34. package/dist/TerrenoProvider.js +10 -1
  35. package/dist/TerrenoProvider.js.map +1 -1
  36. package/dist/UpgradeRequiredScreen.d.ts +8 -0
  37. package/dist/UpgradeRequiredScreen.js +10 -0
  38. package/dist/UpgradeRequiredScreen.js.map +1 -0
  39. package/dist/generateConsentHistoryPdf.d.ts +2 -0
  40. package/dist/generateConsentHistoryPdf.js +185 -0
  41. package/dist/generateConsentHistoryPdf.js.map +1 -0
  42. package/dist/index.d.ts +9 -0
  43. package/dist/index.js +9 -0
  44. package/dist/index.js.map +1 -1
  45. package/dist/useConsentForms.d.ts +29 -0
  46. package/dist/useConsentForms.js +50 -0
  47. package/dist/useConsentForms.js.map +1 -0
  48. package/dist/useConsentHistory.d.ts +31 -0
  49. package/dist/useConsentHistory.js +17 -0
  50. package/dist/useConsentHistory.js.map +1 -0
  51. package/dist/useSubmitConsent.d.ts +12 -0
  52. package/dist/useSubmitConsent.js +23 -0
  53. package/dist/useSubmitConsent.js.map +1 -0
  54. package/package.json +4 -2
  55. package/src/BooleanField.test.tsx +3 -5
  56. package/src/BooleanField.tsx +33 -31
  57. package/src/ConsentFormScreen.tsx +216 -0
  58. package/src/ConsentHistory.tsx +249 -0
  59. package/src/ConsentNavigator.test.tsx +111 -0
  60. package/src/ConsentNavigator.tsx +128 -0
  61. package/src/DataTable.tsx +1 -1
  62. package/src/DateTimeActionSheet.tsx +19 -6
  63. package/src/DateTimeField.tsx +416 -133
  64. package/src/DraggableList.tsx +424 -0
  65. package/src/Link.tsx +1 -1
  66. package/src/MarkdownEditor.tsx +66 -0
  67. package/src/MarkdownEditorField.tsx +32 -28
  68. package/src/Modal.tsx +19 -1
  69. package/src/PickerSelect.tsx +11 -0
  70. package/src/TerrenoProvider.tsx +10 -1
  71. package/src/TimezonePicker.test.tsx +9 -1
  72. package/src/UpgradeRequiredScreen.tsx +52 -0
  73. package/src/__snapshots__/BooleanField.test.tsx.snap +167 -203
  74. package/src/__snapshots__/DataTable.test.tsx.snap +0 -114
  75. package/src/__snapshots__/Field.test.tsx.snap +53 -69
  76. package/src/__snapshots__/Link.test.tsx.snap +14 -21
  77. package/src/__snapshots__/TimezonePicker.test.tsx.snap +0 -4710
  78. package/src/generateConsentHistoryPdf.ts +211 -0
  79. package/src/index.tsx +9 -1
  80. package/src/useConsentForms.ts +70 -0
  81. package/src/useConsentHistory.ts +40 -0
  82. package/src/useSubmitConsent.ts +35 -0
@@ -0,0 +1,424 @@
1
+ // TODO: Open source into @terreno/ui
2
+ // Forked from https://github.com/gerwld/react-native-drag-n-drop-everywhere
3
+ // Because it only supported drag handles on the right, and installing it caused build issues.
4
+
5
+ // MIT License
6
+ // Copyright Patryk Jaworski @gerwld
7
+
8
+ import React, {useMemo, useState} from "react";
9
+ import {Platform, View} from "react-native";
10
+ import {Gesture, GestureDetector} from "react-native-gesture-handler";
11
+ import Animated, {
12
+ runOnJS,
13
+ useAnimatedReaction,
14
+ useAnimatedStyle,
15
+ useSharedValue,
16
+ withSpring,
17
+ } from "react-native-reanimated";
18
+ import {Box} from "./Box";
19
+ import {Icon} from "./Icon";
20
+
21
+ /**
22
+ * Interface representing position mappings of items in the list
23
+ * Maps item IDs to their numeric positions in the list
24
+ */
25
+ interface Positions {
26
+ [key: string]: number;
27
+ }
28
+
29
+ /**
30
+ * Props for an individual draggable item in the list
31
+ */
32
+ interface DragItemProps {
33
+ item: string; // Unique identifier for the item
34
+ index: number; // Original index of the item
35
+ positions: {value: Positions}; // Shared value containing position mappings
36
+ scrollY: {value: number}; // Scroll position value
37
+ itemsGap: number; // Spacing between items
38
+ itemsCount: number; // Total number of items in the list
39
+ itemHeight: number; // Height of each item
40
+ renderItem: (props: {item: string}) => React.ReactElement; // Function to render item content
41
+ renderGrip?: React.ReactElement | (() => React.ReactElement); // Optional drag handle
42
+ passVibration?: () => void; // Optional haptic feedback callback
43
+ itemBorderRadius: number; // Border radius for items
44
+ itemContainerStyle?: any; // Additional styling for item container
45
+ callbackNewDataIds?: (newIds: string[]) => void; // Callback when items are reordered
46
+ backgroundOnHold?: string; // Background color when item is being dragged
47
+ plainPosition: number; // Current position in the list
48
+ }
49
+
50
+ /**
51
+ * Props for the draggable list container component
52
+ */
53
+ interface DragListProps {
54
+ data?: string[]; // Array of item IDs (deprecated, use dataIDs)
55
+ style?: any; // Style for the list container
56
+ callbackNewDataIds: (newIds: string[]) => void; // Callback when items are reordered
57
+ contentContainerStyle?: any; // Style for the content container
58
+ itemContainerStyle?: any; // Style for each item container
59
+ renderItem: (props: {item: string}) => React.ReactElement; // Function to render item content
60
+ renderGrip?: React.ReactElement | (() => React.ReactElement); // Optional custom drag handle
61
+ passVibration?: () => void; // Optional haptic feedback callback
62
+ borderRadius?: number; // Border radius for the list
63
+ backgroundOnHold?: string; // Background color when item is being dragged
64
+ dataIDs?: string[]; // Array of item IDs (preferred over data)
65
+ itemsGap?: number; // Spacing between items
66
+ itemHeight?: number; // Height of each item
67
+ itemBorderRadius?: number; // Border radius for items
68
+ }
69
+
70
+ /**
71
+ * Processes the current positions and calls the callback with the new sorted array
72
+ * This function is marked with "worklet" to allow it to run on the UI thread
73
+ * See: https://docs.swmansion.com/react-native-reanimated/docs/guides/worklets/
74
+ */
75
+ const onCallbackData = (
76
+ positions: Positions,
77
+ callbackNewDataIds: ((newIds: string[]) => void) | undefined,
78
+ prevArrayFromPositions: React.MutableRefObject<string | null>
79
+ ): void => {
80
+ "worklet";
81
+
82
+ // Sort items by their positions and extract the IDs
83
+ const arrayFromPositions = Object.entries(positions)
84
+ .sort(([, indexA], [, indexB]) => {
85
+ const numIndexA = indexA as number;
86
+ const numIndexB = indexB as number;
87
+ return numIndexA - numIndexB;
88
+ })
89
+ .map(([id]) => id);
90
+
91
+ const stringifiedArray = JSON.stringify(arrayFromPositions);
92
+
93
+ // Only call the callback if the array has changed
94
+ if (prevArrayFromPositions.current !== stringifiedArray) {
95
+ prevArrayFromPositions.current = stringifiedArray;
96
+
97
+ if (typeof callbackNewDataIds === "function") {
98
+ callbackNewDataIds(arrayFromPositions);
99
+ }
100
+ }
101
+ };
102
+
103
+ /**
104
+ * Clamps a value between a lower and upper bound
105
+ * Marked as "worklet" to allow it to run on the UI thread
106
+ */
107
+ const clamp = (value: number, lowerBound: number, upperBound: number): number => {
108
+ "worklet";
109
+ return Math.max(lowerBound, Math.min(value, upperBound));
110
+ };
111
+
112
+ /**
113
+ * Swaps the positions of two items in the positions object
114
+ * Used when items are being dragged to new positions
115
+ * Marked as "worklet" to allow it to run on the UI thread
116
+ */
117
+ const objectMove = (object: Positions, from: number, to: number): Positions => {
118
+ "worklet";
119
+ const newObject = Object.assign({}, object);
120
+ for (const id in object) {
121
+ if (object[id] === from) {
122
+ newObject[id] = to;
123
+ }
124
+ if (object[id] === to) {
125
+ newObject[id] = from;
126
+ }
127
+ }
128
+ return newObject;
129
+ };
130
+
131
+ /**
132
+ * Individual draggable item component
133
+ * Handles the drag gesture and animations for a single item in the list
134
+ */
135
+ export const DragItem: React.FC<DragItemProps> = (props) => {
136
+ const {
137
+ item,
138
+ positions,
139
+ scrollY,
140
+ itemsGap,
141
+ itemsCount,
142
+ itemHeight,
143
+ renderItem,
144
+ renderGrip,
145
+ passVibration,
146
+ itemBorderRadius,
147
+ itemContainerStyle,
148
+ callbackNewDataIds,
149
+ backgroundOnHold,
150
+ plainPosition,
151
+ } = props;
152
+
153
+ // Ref to track previous array state to avoid unnecessary callbacks
154
+ const prevArrayFromPositions = React.useRef<string | null>(null);
155
+
156
+ // Shared values for animations and position tracking
157
+ const pressed = useSharedValue(false);
158
+ const offset = useSharedValue(0);
159
+ const startY = useSharedValue(0);
160
+ const top = useSharedValue(plainPosition * (itemHeight + itemsGap));
161
+ const [moving, setMoving] = useState(false);
162
+
163
+ // React to changes in positions when not actively moving
164
+ useAnimatedReaction(
165
+ () => positions.value,
166
+ (currentPositions, prevPositions) => {
167
+ if (currentPositions !== prevPositions && !moving && !top.value) {
168
+ top.value = positions.value[item] * (itemHeight + itemsGap);
169
+ }
170
+ }
171
+ );
172
+
173
+ // React to changes in this item's position when not actively moving
174
+ useAnimatedReaction(
175
+ () => positions.value[item],
176
+ (currentPosition, previousPosition) => {
177
+ if (currentPosition !== previousPosition && !moving) {
178
+ top.value = currentPosition * (itemHeight + itemsGap);
179
+ }
180
+ },
181
+ [moving]
182
+ );
183
+
184
+ const isAndroid = Platform.OS === "android";
185
+
186
+ // Define the animated styles for the item
187
+ const animatedStyles = useAnimatedStyle(() => {
188
+ let topOffset = top.value + offset.value;
189
+ if (Number.isNaN(topOffset)) topOffset = 0;
190
+
191
+ // Basic styles for all platforms
192
+ const anim = {
193
+ backgroundColor: pressed.value ? backgroundOnHold : "transparent",
194
+ height: itemHeight,
195
+ top: topOffset,
196
+ zIndex: pressed.value ? 1 : 0,
197
+ };
198
+
199
+ // Enhanced styles with shadow for iOS
200
+ const animBetter = {
201
+ backgroundColor: pressed.value ? backgroundOnHold : "transparent",
202
+ height: itemHeight,
203
+ shadowOffset: {height: 0, width: 0},
204
+ shadowOpacity: isAndroid ? 0 : withSpring(pressed.value ? 0.2 : 0),
205
+ shadowRadius: isAndroid ? 0 : 5,
206
+ top: topOffset,
207
+ zIndex: pressed.value ? 1 : 0,
208
+ };
209
+
210
+ if (isAndroid) return anim;
211
+ return animBetter;
212
+ });
213
+
214
+ // Define the pan gesture handler for dragging
215
+ const pan = Gesture.Pan()
216
+ .onBegin(() => {
217
+ // When dragging starts
218
+ pressed.value = true;
219
+ runOnJS(setMoving)(true);
220
+ if (passVibration) passVibration();
221
+ startY.value = top.value;
222
+ })
223
+ .onChange((event) => {
224
+ // While dragging
225
+ offset.value = event.translationY;
226
+
227
+ const positionY = startY.value + event.translationY + scrollY.value;
228
+
229
+ // Calculate the new position based on the drag distance
230
+ const newPosition = clamp(Math.floor(positionY / (itemHeight + itemsGap)), 0, itemsCount - 1);
231
+
232
+ // If position changed, update the positions object
233
+ if (newPosition !== positions.value[item]) {
234
+ const newMove = objectMove(positions.value, positions.value[item], newPosition);
235
+ if (newMove && typeof newMove === "object") {
236
+ runOnJS(() => {
237
+ positions.value = newMove;
238
+ })();
239
+ }
240
+
241
+ // Trigger haptic feedback when position changes
242
+ if (typeof passVibration === "function") runOnJS(passVibration)();
243
+ }
244
+ })
245
+ .onFinalize(() => {
246
+ // When dragging ends
247
+ offset.value = 0;
248
+ top.value = positions.value[item] * (itemHeight + itemsGap);
249
+ pressed.value = false;
250
+
251
+ // Notify about the new order
252
+ onCallbackData(positions.value, callbackNewDataIds, prevArrayFromPositions);
253
+
254
+ runOnJS(setMoving)(false);
255
+ });
256
+
257
+ return (
258
+ <Animated.View
259
+ style={[
260
+ {
261
+ alignItems: "center",
262
+ borderRadius: itemBorderRadius,
263
+ flexDirection: "row",
264
+ marginBottom: itemsGap,
265
+ paddingRight: 10,
266
+ position: "absolute",
267
+ width: "100%",
268
+ },
269
+ itemContainerStyle,
270
+ animatedStyles,
271
+ ]}
272
+ >
273
+ <GestureDetector gesture={pan}>
274
+ {renderGrip ? (
275
+ <View
276
+ style={{
277
+ alignItems: "center",
278
+ flexBasis: 45,
279
+ flexGrow: 0,
280
+ flexShrink: 0,
281
+ height: "100%",
282
+ justifyContent: "center",
283
+ minWidth: 45,
284
+ }}
285
+ >
286
+ {typeof renderGrip === "function" ? renderGrip() : renderGrip}
287
+ </View>
288
+ ) : (
289
+ <Box height="100%" justifyContent="center">
290
+ <Icon iconName="arrows-up-down-left-right" />
291
+ </Box>
292
+ )}
293
+ </GestureDetector>
294
+ <View style={{flex: 1}}>{renderItem({item})}</View>
295
+ </Animated.View>
296
+ );
297
+ };
298
+
299
+ /**
300
+ * Main draggable list component that renders a list of draggable items
301
+ * Manages the order and positioning of items
302
+ */
303
+ export const DraggableList: React.FC<DragListProps> = (props) => {
304
+ const {
305
+ data,
306
+ callbackNewDataIds,
307
+ itemContainerStyle,
308
+ renderItem,
309
+ renderGrip,
310
+ passVibration,
311
+ backgroundOnHold = "#e3e3e3",
312
+ } = props;
313
+
314
+ // Use dataIDs prop with fallback to data prop
315
+ const dataIDs = useMemo(() => props?.dataIDs || data || [], [props?.dataIDs, data]);
316
+ const itemsGap = props.itemsGap || 5;
317
+ const itemHeight = props.itemHeight || 50;
318
+ const itemBorderRadius = props.itemBorderRadius || 8;
319
+
320
+ // Validate required props
321
+ if (!dataIDs && !data) {
322
+ throw new Error(
323
+ 'The "dataIDs / data" prop is missing. It should contain an array of identificators of your list items, for example, uuid\'s.'
324
+ );
325
+ }
326
+
327
+ if ((dataIDs || data) && !Array.isArray(dataIDs || data)) {
328
+ throw new Error(
329
+ `The "dataIDs / data" prop should be []. \nProvided:${JSON.stringify(data || dataIDs)}`
330
+ );
331
+ }
332
+
333
+ if (!renderItem) {
334
+ throw new Error(
335
+ 'The "renderItem" prop is missing. You should pass R.C that will render your item based on identificator thar it recieves as {item: id} in the first argument. Example: `function renderItem({item}) {}`'
336
+ );
337
+ }
338
+
339
+ if (!callbackNewDataIds) {
340
+ throw new Error(
341
+ 'The "callbackNewDataIds" prop is missing. You should pass a function that will recieve an array of sorted items IDs. \n\nExample: `function getChanges(newArray) {}`\n\n* Mention: do not change dataIDs argument directly, or it will cause performance issues.`'
342
+ );
343
+ }
344
+
345
+ if (typeof callbackNewDataIds !== "function") {
346
+ throw new Error(
347
+ `The "callbackNewDataIds" prop should be function type. \nProvided: ${JSON.stringify(
348
+ renderGrip
349
+ )}`
350
+ );
351
+ }
352
+
353
+ // Function to extract key from item ID
354
+ const keyExtractor = (id: string): string => id;
355
+
356
+ /**
357
+ * Converts an array of item IDs to a positions object
358
+ * Maps each ID to its index in the array
359
+ */
360
+ const listToObject = useMemo(() => {
361
+ return (list: string[]): Positions => {
362
+ const object: Positions = {};
363
+ list.forEach((item, i) => {
364
+ object[item] = i;
365
+ });
366
+ return object;
367
+ };
368
+ }, []);
369
+
370
+ // Shared values for tracking positions and scroll
371
+ const positions = useSharedValue(listToObject(dataIDs));
372
+ const scrollY = useSharedValue(0);
373
+
374
+ // Mirror positions.value to React state for safe access in render
375
+ const [plainPositions, setPlainPositions] = React.useState<Positions>(() =>
376
+ listToObject(dataIDs)
377
+ );
378
+
379
+ // Update plainPositions state when shared positions value changes
380
+ useAnimatedReaction(
381
+ () => positions.value,
382
+ (currentPositions) => {
383
+ runOnJS(setPlainPositions)({...currentPositions});
384
+ },
385
+ []
386
+ );
387
+
388
+ // Update positions when dataIDs changes
389
+ // This effect ensures the positions shared value is updated when the dataIDs prop changes
390
+ React.useEffect(() => {
391
+ positions.value = listToObject(dataIDs);
392
+ }, [dataIDs, positions, listToObject]);
393
+
394
+ return (
395
+ <View
396
+ style={{
397
+ minHeight: dataIDs.length * itemHeight + (dataIDs.length - 1) * itemsGap,
398
+ position: "relative",
399
+ width: "100%",
400
+ }}
401
+ >
402
+ {dataIDs?.map((item, index) => (
403
+ <DragItem
404
+ backgroundOnHold={backgroundOnHold}
405
+ callbackNewDataIds={callbackNewDataIds}
406
+ index={index}
407
+ item={item}
408
+ itemBorderRadius={itemBorderRadius}
409
+ itemContainerStyle={itemContainerStyle}
410
+ itemHeight={itemHeight}
411
+ itemsCount={dataIDs.length}
412
+ itemsGap={itemsGap}
413
+ key={keyExtractor(item)}
414
+ passVibration={passVibration}
415
+ plainPosition={plainPositions[item] ?? 0}
416
+ positions={positions}
417
+ renderGrip={renderGrip}
418
+ renderItem={renderItem}
419
+ scrollY={scrollY}
420
+ />
421
+ ))}
422
+ </View>
423
+ );
424
+ };
package/src/Link.tsx CHANGED
@@ -14,7 +14,7 @@ export const Link = ({text, href, onClick}: LinkProps): React.ReactElement => {
14
14
  hitSlop={20}
15
15
  onPress={() => (onClick ? onClick() : href && Linking.openURL(href))}
16
16
  >
17
- <Text color="link" underline>
17
+ <Text color="link" skipLinking underline>
18
18
  {text}
19
19
  </Text>
20
20
  </Pressable>
@@ -0,0 +1,66 @@
1
+ import React from "react";
2
+ import {ScrollView, useWindowDimensions} from "react-native";
3
+ import {Box} from "./Box";
4
+ import {Heading} from "./Heading";
5
+ import {MarkdownView} from "./MarkdownView";
6
+ import {TextField} from "./TextField";
7
+
8
+ interface MarkdownEditorProps {
9
+ value: string;
10
+ onChange: (value: string) => void;
11
+ placeholder?: string;
12
+ title?: string;
13
+ disabled?: boolean;
14
+ testID?: string;
15
+ maxHeight?: number;
16
+ }
17
+
18
+ export const MarkdownEditor: React.FC<MarkdownEditorProps> = ({
19
+ value,
20
+ onChange,
21
+ placeholder,
22
+ title,
23
+ disabled,
24
+ testID,
25
+ maxHeight = 500,
26
+ }) => {
27
+ const {width} = useWindowDimensions();
28
+ const isDesktop = width >= 768;
29
+
30
+ return (
31
+ <Box direction="column" gap={2} testID={testID}>
32
+ {Boolean(title) && <Heading size="sm">{title}</Heading>}
33
+ <Box direction={isDesktop ? "row" : "column"} gap={3}>
34
+ <Box flex="grow">
35
+ <Heading size="sm">Edit</Heading>
36
+ <Box marginTop={1}>
37
+ <TextField
38
+ disabled={disabled}
39
+ grow
40
+ multiline
41
+ onChange={onChange}
42
+ placeholder={placeholder}
43
+ rows={10}
44
+ testID={testID ? `${testID}-input` : undefined}
45
+ value={value}
46
+ />
47
+ </Box>
48
+ </Box>
49
+ <Box flex="grow">
50
+ <Heading size="sm">Preview</Heading>
51
+ <ScrollView style={{maxHeight, minHeight: 100}}>
52
+ <Box
53
+ border="default"
54
+ marginTop={1}
55
+ padding={3}
56
+ rounding="sm"
57
+ testID={testID ? `${testID}-preview` : undefined}
58
+ >
59
+ <MarkdownView>{value || " "}</MarkdownView>
60
+ </Box>
61
+ </ScrollView>
62
+ </Box>
63
+ </Box>
64
+ </Box>
65
+ );
66
+ };
@@ -1,6 +1,6 @@
1
1
  import type React from "react";
2
2
  import {useMemo, useRef} from "react";
3
- import {Platform, Pressable, Text as RNText, TextInput, View} from "react-native";
3
+ import {Platform, Pressable, Text as RNText, ScrollView, TextInput, View} from "react-native";
4
4
 
5
5
  import {Box} from "./Box";
6
6
  import type {ErrorTextProps, HelperTextProps} from "./Common";
@@ -16,6 +16,7 @@ interface MarkdownEditorFieldProps extends HelperTextProps, ErrorTextProps {
16
16
  placeholder?: string;
17
17
  disabled?: boolean;
18
18
  testID?: string;
19
+ maxHeight?: number;
19
20
  }
20
21
 
21
22
  interface ToolbarButton {
@@ -44,6 +45,7 @@ export const MarkdownEditorField: React.FC<MarkdownEditorFieldProps> = ({
44
45
  errorText,
45
46
  helperText,
46
47
  testID,
48
+ maxHeight = 500,
47
49
  }) => {
48
50
  const {theme} = useTheme();
49
51
  const isWeb = Platform.OS === "web";
@@ -64,30 +66,32 @@ export const MarkdownEditorField: React.FC<MarkdownEditorFieldProps> = ({
64
66
  overflow="hidden"
65
67
  rounding="md"
66
68
  >
67
- <View style={{flex: 1, minHeight: 200}}>
68
- <TextInput
69
- editable={!disabled}
70
- multiline
71
- onChangeText={onChange}
72
- placeholder={placeholder ?? "Enter markdown..."}
73
- placeholderTextColor={theme.text.secondaryDark}
74
- ref={inputRef}
75
- style={{
76
- backgroundColor: theme.surface.base,
77
- borderBottomWidth: isWeb ? 0 : 1,
78
- borderColor: theme.border.default,
79
- borderRightWidth: isWeb ? 1 : 0,
80
- color: theme.text.primary,
81
- flex: 1,
82
- fontFamily: monoFont,
83
- fontSize: 14,
84
- minHeight: 200,
85
- padding: 12,
86
- textAlignVertical: "top",
87
- }}
88
- testID={testID ? `${testID}-input` : undefined}
89
- value={value}
90
- />
69
+ <View style={{flex: 1, maxHeight, minHeight: 200}}>
70
+ <ScrollView style={{flex: 1}}>
71
+ <TextInput
72
+ editable={!disabled}
73
+ multiline
74
+ onChangeText={onChange}
75
+ placeholder={placeholder ?? "Enter markdown..."}
76
+ placeholderTextColor={theme.text.secondaryDark}
77
+ ref={inputRef}
78
+ style={{
79
+ backgroundColor: theme.surface.base,
80
+ borderBottomWidth: isWeb ? 0 : 1,
81
+ borderColor: theme.border.default,
82
+ borderRightWidth: isWeb ? 1 : 0,
83
+ color: theme.text.primary,
84
+ flex: 1,
85
+ fontFamily: monoFont,
86
+ fontSize: 14,
87
+ minHeight: 200,
88
+ padding: 12,
89
+ textAlignVertical: "top",
90
+ }}
91
+ testID={testID ? `${testID}-input` : undefined}
92
+ value={value}
93
+ />
94
+ </ScrollView>
91
95
  {!disabled && (
92
96
  <View
93
97
  style={{
@@ -135,8 +139,8 @@ export const MarkdownEditorField: React.FC<MarkdownEditorFieldProps> = ({
135
139
  </View>
136
140
  )}
137
141
  </View>
138
- <View style={{flex: 1, minHeight: 200}}>
139
- <Box color="base" padding={3} style={{flex: 1, minHeight: 200}}>
142
+ <ScrollView style={{flex: 1, maxHeight, minHeight: 200}}>
143
+ <Box color="base" padding={3} style={{minHeight: 200}}>
140
144
  {value ? (
141
145
  <MarkdownView>{value}</MarkdownView>
142
146
  ) : (
@@ -145,7 +149,7 @@ export const MarkdownEditorField: React.FC<MarkdownEditorFieldProps> = ({
145
149
  </Text>
146
150
  )}
147
151
  </Box>
148
- </View>
152
+ </ScrollView>
149
153
  </Box>
150
154
  {errorText && <FieldError text={errorText} />}
151
155
  {helperText && <FieldHelperText text={helperText} />}
package/src/Modal.tsx CHANGED
@@ -1,5 +1,12 @@
1
1
  import {type FC, useEffect, useRef} from "react";
2
- import {Dimensions, type DimensionValue, Pressable, Modal as RNModal, View} from "react-native";
2
+ import {
3
+ Dimensions,
4
+ type DimensionValue,
5
+ Platform,
6
+ Pressable,
7
+ Modal as RNModal,
8
+ View,
9
+ } from "react-native";
3
10
  import ActionSheet, {type ActionSheetRef} from "react-native-actions-sheet";
4
11
  import {Gesture, GestureDetector} from "react-native-gesture-handler";
5
12
  import {runOnJS} from "react-native-reanimated";
@@ -211,6 +218,17 @@ export const Modal: FC<ModalProps> = ({
211
218
  }
212
219
  });
213
220
 
221
+ // On web, blur the active element before the modal opens to prevent
222
+ // "aria-hidden on a focused element" warnings from React Native Web.
223
+ useEffect(() => {
224
+ if (visible && Platform.OS === "web") {
225
+ const active = document.activeElement;
226
+ if (active instanceof HTMLElement) {
227
+ active.blur();
228
+ }
229
+ }
230
+ }, [visible]);
231
+
214
232
  // Open the action sheet ref when the visible prop changes.
215
233
  useEffect(() => {
216
234
  if (actionSheetRef.current) {
@@ -129,6 +129,17 @@ export function RNPickerSelect({
129
129
  const [doneDepressed, setDoneDepressed] = useState<boolean>(false);
130
130
  const {theme} = useTheme();
131
131
 
132
+ // On web, blur the active element before the picker modal opens to prevent
133
+ // "aria-hidden on a focused element" warnings from React Native Web.
134
+ useEffect(() => {
135
+ if (showPicker && Platform.OS === "web") {
136
+ const active = document.activeElement;
137
+ if (active instanceof HTMLElement) {
138
+ active.blur();
139
+ }
140
+ }
141
+ }, [showPicker]);
142
+
132
143
  const options = useMemo(() => {
133
144
  if (isEqual(placeholder, {})) {
134
145
  return [...items];
@@ -19,7 +19,16 @@ export const TerrenoProvider: FC<{
19
19
  duration={50000}
20
20
  offset={50}
21
21
  placement="bottom"
22
- renderToast={(toastOptions) => <Toast {...toastOptions?.data} />}
22
+ renderToast={(toastOptions) => {
23
+ const dataOnDismiss = toastOptions?.data?.onDismiss;
24
+ const providerOnHide = toastOptions?.onHide;
25
+ const handleDismiss = () => {
26
+ dataOnDismiss?.();
27
+ providerOnHide?.();
28
+ };
29
+
30
+ return <Toast {...toastOptions?.data} onDismiss={handleDismiss} />;
31
+ }}
23
32
  swipeEnabled
24
33
  >
25
34
  <OpenAPIProvider specUrl={openAPISpecUrl}>
@@ -37,7 +37,15 @@ describe("TimezonePicker", () => {
37
37
  const {toJSON} = renderWithTheme(
38
38
  <TimezonePicker location="Worldwide" onChange={() => {}} timezone="Europe/London" />
39
39
  );
40
- expect(toJSON()).toMatchSnapshot();
40
+ const json = toJSON();
41
+ expect(json).toBeTruthy();
42
+
43
+ // Worldwide list is derived from Intl.supportedValuesOf("timeZone") which varies
44
+ // by runtime ICU data, so we assert structure rather than a full snapshot.
45
+ const rendered = JSON.stringify(json);
46
+ expect(rendered).toContain("Europe/London");
47
+ expect(rendered).toContain("America/New_York");
48
+ expect(rendered).toContain("Asia/Tokyo");
41
49
  });
42
50
 
43
51
  it("renders with short timezone labels", () => {