@terreno/ui 0.7.2 → 0.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.
- package/dist/BooleanField.js +23 -23
- package/dist/BooleanField.js.map +1 -1
- package/dist/ConsentFormScreen.d.ts +14 -0
- package/dist/ConsentFormScreen.js +93 -0
- package/dist/ConsentFormScreen.js.map +1 -0
- package/dist/ConsentHistory.d.ts +8 -0
- package/dist/ConsentHistory.js +70 -0
- package/dist/ConsentHistory.js.map +1 -0
- package/dist/ConsentNavigator.d.ts +9 -0
- package/dist/ConsentNavigator.js +72 -0
- package/dist/ConsentNavigator.js.map +1 -0
- package/dist/DataTable.js +1 -1
- package/dist/DataTable.js.map +1 -1
- package/dist/DateTimeActionSheet.js +22 -6
- package/dist/DateTimeActionSheet.js.map +1 -1
- package/dist/DateTimeField.d.ts +22 -0
- package/dist/DateTimeField.js +187 -67
- package/dist/DateTimeField.js.map +1 -1
- package/dist/DraggableList.d.ts +66 -0
- package/dist/DraggableList.js +241 -0
- package/dist/DraggableList.js.map +1 -0
- package/dist/Link.js +1 -1
- package/dist/Link.js.map +1 -1
- package/dist/MarkdownEditor.d.ts +12 -0
- package/dist/MarkdownEditor.js +12 -0
- package/dist/MarkdownEditor.js.map +1 -0
- package/dist/MarkdownEditorField.d.ts +1 -0
- package/dist/MarkdownEditorField.js +16 -16
- package/dist/MarkdownEditorField.js.map +1 -1
- package/dist/Modal.js +11 -1
- package/dist/Modal.js.map +1 -1
- package/dist/PickerSelect.js +10 -0
- package/dist/PickerSelect.js.map +1 -1
- package/dist/TerrenoProvider.js +10 -1
- package/dist/TerrenoProvider.js.map +1 -1
- package/dist/UpgradeRequiredScreen.d.ts +8 -0
- package/dist/UpgradeRequiredScreen.js +10 -0
- package/dist/UpgradeRequiredScreen.js.map +1 -0
- package/dist/generateConsentHistoryPdf.d.ts +2 -0
- package/dist/generateConsentHistoryPdf.js +185 -0
- package/dist/generateConsentHistoryPdf.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -1
- package/dist/useConsentForms.d.ts +29 -0
- package/dist/useConsentForms.js +50 -0
- package/dist/useConsentForms.js.map +1 -0
- package/dist/useConsentHistory.d.ts +31 -0
- package/dist/useConsentHistory.js +17 -0
- package/dist/useConsentHistory.js.map +1 -0
- package/dist/useSubmitConsent.d.ts +12 -0
- package/dist/useSubmitConsent.js +23 -0
- package/dist/useSubmitConsent.js.map +1 -0
- package/package.json +4 -2
- package/src/BooleanField.test.tsx +3 -5
- package/src/BooleanField.tsx +33 -31
- package/src/ConsentFormScreen.tsx +216 -0
- package/src/ConsentHistory.tsx +249 -0
- package/src/ConsentNavigator.test.tsx +111 -0
- package/src/ConsentNavigator.tsx +128 -0
- package/src/DataTable.tsx +1 -1
- package/src/DateTimeActionSheet.tsx +19 -6
- package/src/DateTimeField.tsx +416 -133
- package/src/DraggableList.tsx +424 -0
- package/src/Link.tsx +1 -1
- package/src/MarkdownEditor.tsx +66 -0
- package/src/MarkdownEditorField.tsx +32 -28
- package/src/Modal.tsx +19 -1
- package/src/PickerSelect.tsx +11 -0
- package/src/TerrenoProvider.tsx +10 -1
- package/src/TimezonePicker.test.tsx +9 -1
- package/src/UpgradeRequiredScreen.tsx +52 -0
- package/src/__snapshots__/BooleanField.test.tsx.snap +167 -203
- package/src/__snapshots__/DataTable.test.tsx.snap +0 -114
- package/src/__snapshots__/Field.test.tsx.snap +53 -69
- package/src/__snapshots__/Link.test.tsx.snap +14 -21
- package/src/__snapshots__/TimezonePicker.test.tsx.snap +0 -4710
- package/src/generateConsentHistoryPdf.ts +211 -0
- package/src/index.tsx +9 -1
- package/src/useConsentForms.ts +70 -0
- package/src/useConsentHistory.ts +40 -0
- 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
|
-
<
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
<
|
|
139
|
-
<Box color="base" padding={3} style={{
|
|
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
|
-
</
|
|
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 {
|
|
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) {
|
package/src/PickerSelect.tsx
CHANGED
|
@@ -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];
|
package/src/TerrenoProvider.tsx
CHANGED
|
@@ -19,7 +19,16 @@ export const TerrenoProvider: FC<{
|
|
|
19
19
|
duration={50000}
|
|
20
20
|
offset={50}
|
|
21
21
|
placement="bottom"
|
|
22
|
-
renderToast={(toastOptions) =>
|
|
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
|
-
|
|
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", () => {
|