ferns-ui 0.27.1 → 0.28.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/Common.d.ts +1 -1
- package/dist/Common.js.map +1 -1
- package/dist/DateTimeActionSheet.d.ts +4 -2
- package/dist/DateTimeActionSheet.js +216 -20
- package/dist/DateTimeActionSheet.js.map +1 -1
- package/dist/Field.d.ts +1 -1
- package/dist/Field.js +4 -5
- package/dist/Field.js.map +1 -1
- package/dist/MediaQuery.d.ts +1 -0
- package/dist/MediaQuery.js +3 -0
- package/dist/MediaQuery.js.map +1 -1
- package/dist/Modal.d.ts +2 -1
- package/dist/Modal.js +75 -35
- package/dist/Modal.js.map +1 -1
- package/dist/PickerSelect.js +1 -1
- package/dist/PickerSelect.js.map +1 -1
- package/dist/TapToEdit.js +4 -1
- package/dist/TapToEdit.js.map +1 -1
- package/dist/TextField.js +55 -62
- package/dist/TextField.js.map +1 -1
- package/package.json +11 -10
- package/src/Common.ts +2 -0
- package/src/DateTimeActionSheet.tsx +360 -36
- package/src/Field.tsx +20 -6
- package/src/MediaQuery.ts +4 -0
- package/src/Modal.tsx +150 -54
- package/src/PickerSelect.tsx +5 -1
- package/src/TapToEdit.tsx +4 -1
- package/src/TextField.tsx +72 -103
|
@@ -1,55 +1,379 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
1
|
+
import {Picker} from "@react-native-picker/picker";
|
|
2
|
+
import range from "lodash/range";
|
|
3
|
+
import moment from "moment";
|
|
4
|
+
import React, {useState} from "react";
|
|
5
|
+
import {Platform, StyleProp, TextInput, TextStyle, View} from "react-native";
|
|
6
|
+
import {Calendar} from "react-native-calendars";
|
|
4
7
|
|
|
5
|
-
import {ActionSheet} from "./ActionSheet";
|
|
6
8
|
import {Box} from "./Box";
|
|
7
|
-
import {Button} from "./Button";
|
|
8
9
|
import {OnChangeCallback} from "./Common";
|
|
10
|
+
import {Heading} from "./Heading";
|
|
11
|
+
import {IconButton} from "./IconButton";
|
|
12
|
+
import {isMobileDevice} from "./MediaQuery";
|
|
13
|
+
import {Modal} from "./Modal";
|
|
14
|
+
import {SelectList} from "./SelectList";
|
|
15
|
+
import {Unifier} from "./Unifier";
|
|
16
|
+
|
|
17
|
+
const TIME_PICKER_HEIGHT = 104;
|
|
18
|
+
const INPUT_HEIGHT = 40;
|
|
19
|
+
|
|
20
|
+
const hours = range(1, 13).map((n) => String(n));
|
|
21
|
+
// TODO: support limited picker minutes, e.g. 5 or 15 minute increments.
|
|
22
|
+
const minutes = range(0, 60).map((n) => String(n).padStart(2, "0"));
|
|
23
|
+
const minutesOptions = [...minutes, "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"];
|
|
24
|
+
|
|
25
|
+
function TimeInput({
|
|
26
|
+
type,
|
|
27
|
+
value,
|
|
28
|
+
onChange,
|
|
29
|
+
}: {
|
|
30
|
+
type: "hour" | "minute";
|
|
31
|
+
value: number;
|
|
32
|
+
onChange: (value: number) => void;
|
|
33
|
+
}): React.ReactElement {
|
|
34
|
+
const defaultText = type === "minute" ? String(value).padStart(2, "0") : String(value);
|
|
35
|
+
const [text, setText] = useState(defaultText);
|
|
36
|
+
const [focused, setFocused] = useState(false);
|
|
37
|
+
let error = false;
|
|
38
|
+
if (type === "hour") {
|
|
39
|
+
error = !hours.includes(String(Number(text)));
|
|
40
|
+
} else if (type === "minute") {
|
|
41
|
+
error = !minutesOptions.includes(String(Number(text)));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Broken out because types don't think "outline" is a valid style.
|
|
45
|
+
const textInputStyle: StyleProp<TextStyle> = {
|
|
46
|
+
flex: 1,
|
|
47
|
+
paddingTop: 4,
|
|
48
|
+
paddingRight: 4,
|
|
49
|
+
paddingBottom: 4,
|
|
50
|
+
paddingLeft: 0,
|
|
51
|
+
height: INPUT_HEIGHT,
|
|
52
|
+
width: "100%",
|
|
53
|
+
color: Unifier.theme.darkGray,
|
|
54
|
+
fontFamily: Unifier.theme.primaryFont,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<View
|
|
59
|
+
style={{
|
|
60
|
+
flexDirection: "row",
|
|
61
|
+
justifyContent: "center",
|
|
62
|
+
alignItems: "center",
|
|
63
|
+
height: INPUT_HEIGHT,
|
|
64
|
+
width: "100%",
|
|
65
|
+
// Add padding so the border doesn't mess up layouts
|
|
66
|
+
paddingHorizontal: focused ? 10 : 14,
|
|
67
|
+
paddingVertical: focused ? 0 : 4,
|
|
68
|
+
borderColor: error ? Unifier.theme.red : Unifier.theme.blue,
|
|
69
|
+
borderWidth: focused ? 5 : 1,
|
|
70
|
+
borderRadius: 5,
|
|
71
|
+
backgroundColor: Unifier.theme.white,
|
|
72
|
+
}}
|
|
73
|
+
>
|
|
74
|
+
<TextInput
|
|
75
|
+
keyboardType="number-pad"
|
|
76
|
+
returnKeyType="done"
|
|
77
|
+
style={
|
|
78
|
+
{
|
|
79
|
+
...textInputStyle,
|
|
80
|
+
outline: Platform.select({web: "none"}),
|
|
81
|
+
} as any
|
|
82
|
+
}
|
|
83
|
+
textContentType="none"
|
|
84
|
+
underlineColorAndroid="transparent"
|
|
85
|
+
value={text}
|
|
86
|
+
onBlur={() => {
|
|
87
|
+
setFocused(false);
|
|
88
|
+
}}
|
|
89
|
+
onChangeText={(t) => {
|
|
90
|
+
setText(t);
|
|
91
|
+
onChange(Number(t));
|
|
92
|
+
}}
|
|
93
|
+
onFocus={() => {
|
|
94
|
+
setFocused(true);
|
|
95
|
+
}}
|
|
96
|
+
/>
|
|
97
|
+
</View>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function CalendarHeader({
|
|
102
|
+
addMonth,
|
|
103
|
+
month,
|
|
104
|
+
}: {
|
|
105
|
+
addMonth: (num: number) => void;
|
|
106
|
+
month: Date[];
|
|
107
|
+
}): React.ReactElement {
|
|
108
|
+
const displayDate = moment(month[0]).format("MMM YYYY");
|
|
109
|
+
return (
|
|
110
|
+
<Box alignItems="center" direction="row" height={40} justifyContent="between" width="100%">
|
|
111
|
+
<IconButton
|
|
112
|
+
accessibilityLabel="arrow"
|
|
113
|
+
bgColor="white"
|
|
114
|
+
icon="angle-double-left"
|
|
115
|
+
iconColor="primary"
|
|
116
|
+
size="md"
|
|
117
|
+
onClick={() => {
|
|
118
|
+
addMonth(-12);
|
|
119
|
+
}}
|
|
120
|
+
/>
|
|
121
|
+
<IconButton
|
|
122
|
+
accessibilityLabel="arrow"
|
|
123
|
+
bgColor="white"
|
|
124
|
+
icon="angle-left"
|
|
125
|
+
iconColor="primary"
|
|
126
|
+
size="md"
|
|
127
|
+
onClick={() => {
|
|
128
|
+
addMonth(-1);
|
|
129
|
+
}}
|
|
130
|
+
/>
|
|
131
|
+
<Heading size="sm">{displayDate}</Heading>
|
|
132
|
+
<IconButton
|
|
133
|
+
accessibilityLabel="arrow"
|
|
134
|
+
bgColor="white"
|
|
135
|
+
icon="angle-right"
|
|
136
|
+
iconColor="primary"
|
|
137
|
+
size="md"
|
|
138
|
+
onClick={() => {
|
|
139
|
+
addMonth(1);
|
|
140
|
+
}}
|
|
141
|
+
/>
|
|
142
|
+
<IconButton
|
|
143
|
+
accessibilityLabel="arrow"
|
|
144
|
+
bgColor="white"
|
|
145
|
+
icon="angle-double-right"
|
|
146
|
+
iconColor="primary"
|
|
147
|
+
size="md"
|
|
148
|
+
onClick={() => {
|
|
149
|
+
addMonth(12);
|
|
150
|
+
}}
|
|
151
|
+
/>
|
|
152
|
+
</Box>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
9
155
|
|
|
10
156
|
interface DateTimeActionSheetProps {
|
|
11
157
|
value?: string;
|
|
12
|
-
mode?: "date" | "time";
|
|
158
|
+
mode?: "date" | "time" | "datetime";
|
|
159
|
+
// Returns an ISO 8601 string. If mode is "time", the date portion is today.
|
|
13
160
|
onChange: OnChangeCallback;
|
|
14
161
|
actionSheetRef: React.RefObject<any>;
|
|
162
|
+
visible: boolean;
|
|
163
|
+
onDismiss: () => void;
|
|
15
164
|
}
|
|
16
165
|
|
|
166
|
+
// For mobile, renders all components in an action sheet.
|
|
167
|
+
// For web, renders all components in a modal.
|
|
168
|
+
// For mobile:
|
|
169
|
+
// If mode is "time", renders a spinner picker for time picker on both platforms.
|
|
170
|
+
// If mode is "date", renders our custom calendar on both platforms.
|
|
171
|
+
// If mode is "datetime", renders a spinner picker for time picker and our custom calendar on both platforms.
|
|
172
|
+
// For web, renders a simplistic text box for time picker and a calendar for date picker in a modal
|
|
173
|
+
// In the future, web time picker should be a typeahead dropdown like Google calendar.
|
|
17
174
|
export function DateTimeActionSheet({
|
|
18
|
-
actionSheetRef,
|
|
175
|
+
// actionSheetRef,
|
|
19
176
|
mode,
|
|
20
177
|
value,
|
|
21
178
|
onChange,
|
|
179
|
+
visible,
|
|
180
|
+
onDismiss,
|
|
22
181
|
}: DateTimeActionSheetProps) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
182
|
+
// Accept ISO 8601, HH:mm, or hh:mm A formats. We may want only HH:mm or hh:mm A for mode=time
|
|
183
|
+
const m = moment(value, [moment.ISO_8601, "HH:mm", "hh:mm A"]);
|
|
184
|
+
|
|
185
|
+
if (!m.isValid()) {
|
|
186
|
+
throw new Error(`Invalid date/time value ${value}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
let hr = moment(m).hour() % 12;
|
|
190
|
+
if (hr === 0) {
|
|
191
|
+
hr = 12;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const [hour, setHour] = useState<number>(hr);
|
|
195
|
+
const [minute, setMinute] = useState<number>(moment(m).minute());
|
|
196
|
+
const [amPm, setAmPm] = useState<"am" | "pm">(moment(m).format("a") === "am" ? "am" : "pm");
|
|
197
|
+
const [date, setDate] = useState<string>(moment(m).toISOString());
|
|
198
|
+
|
|
199
|
+
// TODO Support 24 hour time for time picker.
|
|
200
|
+
const renderMobileTime = () => {
|
|
201
|
+
return (
|
|
202
|
+
<Box direction="row" width="100%">
|
|
203
|
+
<Box paddingY={2} width="35%">
|
|
204
|
+
<Picker
|
|
205
|
+
itemStyle={{
|
|
206
|
+
height: TIME_PICKER_HEIGHT,
|
|
207
|
+
}}
|
|
208
|
+
selectedValue={hour}
|
|
209
|
+
style={{
|
|
210
|
+
height: TIME_PICKER_HEIGHT,
|
|
211
|
+
backgroundColor: "#FFFFFF",
|
|
212
|
+
}}
|
|
213
|
+
onValueChange={(itemValue) => setHour(itemValue)}
|
|
214
|
+
>
|
|
215
|
+
{hours.map((n) => (
|
|
216
|
+
<Picker.Item key={String(n)} label={String(n)} value={String(n)} />
|
|
217
|
+
))}
|
|
218
|
+
</Picker>
|
|
219
|
+
</Box>
|
|
220
|
+
<Box paddingY={2} width="35%">
|
|
221
|
+
<Picker
|
|
222
|
+
itemStyle={{
|
|
223
|
+
height: TIME_PICKER_HEIGHT,
|
|
224
|
+
}}
|
|
225
|
+
selectedValue={minute}
|
|
226
|
+
style={{
|
|
227
|
+
height: TIME_PICKER_HEIGHT,
|
|
228
|
+
backgroundColor: "#FFFFFF",
|
|
229
|
+
}}
|
|
230
|
+
onValueChange={(itemValue) => setMinute(itemValue)}
|
|
231
|
+
>
|
|
232
|
+
{minutes.map((n) => (
|
|
233
|
+
<Picker.Item key={String(n)} label={String(n)} value={String(n)} />
|
|
234
|
+
))}
|
|
235
|
+
</Picker>
|
|
236
|
+
</Box>
|
|
237
|
+
<Box paddingY={2} width="30%">
|
|
238
|
+
<Picker
|
|
239
|
+
itemStyle={{
|
|
240
|
+
height: TIME_PICKER_HEIGHT,
|
|
241
|
+
}}
|
|
242
|
+
selectedValue={amPm}
|
|
243
|
+
style={{
|
|
244
|
+
height: TIME_PICKER_HEIGHT,
|
|
245
|
+
backgroundColor: "#FFFFFF",
|
|
246
|
+
}}
|
|
247
|
+
onValueChange={(itemValue) => setAmPm(itemValue)}
|
|
248
|
+
>
|
|
249
|
+
<Picker.Item key="am" label="am" value="am" />
|
|
250
|
+
<Picker.Item key="pm" label="pm" value="pm" />
|
|
251
|
+
</Picker>
|
|
38
252
|
</Box>
|
|
39
|
-
<DateTimePicker
|
|
40
|
-
display="spinner"
|
|
41
|
-
is24Hour
|
|
42
|
-
mode={mode}
|
|
43
|
-
testID="dateTimePicker"
|
|
44
|
-
value={moment(value).toDate()}
|
|
45
|
-
onChange={(event: any, date: any) => {
|
|
46
|
-
if (!date) {
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
onChange({event, value: date.toString()});
|
|
50
|
-
}}
|
|
51
|
-
/>
|
|
52
253
|
</Box>
|
|
53
|
-
|
|
254
|
+
);
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
// TODO: Support a typeahead dropdown for time picker, similar to Google Calendar on the web.
|
|
258
|
+
const renderWebTime = () => {
|
|
259
|
+
return (
|
|
260
|
+
<Box direction="row" justifyContent="center" width="100%">
|
|
261
|
+
<Box width={60}>
|
|
262
|
+
<TimeInput type="hour" value={hour} onChange={(v) => setHour(v)} />
|
|
263
|
+
</Box>
|
|
264
|
+
<Box
|
|
265
|
+
alignItems="center"
|
|
266
|
+
height={INPUT_HEIGHT}
|
|
267
|
+
justifyContent="center"
|
|
268
|
+
marginLeft={2}
|
|
269
|
+
marginRight={2}
|
|
270
|
+
>
|
|
271
|
+
<Heading size="md">:</Heading>
|
|
272
|
+
</Box>
|
|
273
|
+
<Box marginRight={2} width={60}>
|
|
274
|
+
<TimeInput type="minute" value={minute} onChange={(v) => setMinute(v)} />
|
|
275
|
+
</Box>
|
|
276
|
+
|
|
277
|
+
<Box width={60}>
|
|
278
|
+
<SelectList
|
|
279
|
+
options={[
|
|
280
|
+
{label: "am", value: "am"},
|
|
281
|
+
{label: "pm", value: "pm"},
|
|
282
|
+
]}
|
|
283
|
+
style={{minHeight: INPUT_HEIGHT}}
|
|
284
|
+
value={amPm}
|
|
285
|
+
onChange={(result) => {
|
|
286
|
+
setAmPm(result as "am" | "pm");
|
|
287
|
+
}}
|
|
288
|
+
/>
|
|
289
|
+
</Box>
|
|
290
|
+
</Box>
|
|
291
|
+
);
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
const renderDateTime = (): React.ReactElement => {
|
|
295
|
+
return (
|
|
296
|
+
<Box>
|
|
297
|
+
<Box marginBottom={2}>{renderDateCalendar()}</Box>
|
|
298
|
+
{isMobileDevice() ? renderMobileTime() : renderWebTime()}
|
|
299
|
+
</Box>
|
|
300
|
+
);
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
// Note: do not call this if waiting on a state change.
|
|
304
|
+
const sendOnChange = () => {
|
|
305
|
+
const hourChange = amPm === "pm" && hour !== 12 ? Number(hour) + 12 : Number(hour);
|
|
306
|
+
if (mode === "date") {
|
|
307
|
+
onChange({value: date});
|
|
308
|
+
} else if (mode === "time") {
|
|
309
|
+
onChange({
|
|
310
|
+
value: moment().hour(hourChange).minute(Number(minute)).toISOString(),
|
|
311
|
+
});
|
|
312
|
+
} else if (mode === "datetime") {
|
|
313
|
+
onChange({
|
|
314
|
+
value: moment(date).hour(hourChange).minute(Number(minute)).toISOString(),
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
onDismiss();
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
// Renders our custom calendar component on mobile or web.
|
|
321
|
+
const renderDateCalendar = () => {
|
|
322
|
+
const markedDates = {};
|
|
323
|
+
if (date) {
|
|
324
|
+
markedDates[moment(date).format("YYYY-MM-DD")] = {
|
|
325
|
+
selected: true,
|
|
326
|
+
selectedColor: Unifier.theme.primary,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
return (
|
|
330
|
+
<Calendar
|
|
331
|
+
customHeader={CalendarHeader}
|
|
332
|
+
initialDate={moment(date).format("YYYY-MM-DD")}
|
|
333
|
+
markedDates={markedDates}
|
|
334
|
+
onDayPress={(day) => {
|
|
335
|
+
setDate(day.dateString);
|
|
336
|
+
// If mode is just date, we can shortcut and close right away. time and datetime need to wait for the
|
|
337
|
+
// primary button.
|
|
338
|
+
if (mode === "date") {
|
|
339
|
+
onChange({value: day.dateString});
|
|
340
|
+
onDismiss();
|
|
341
|
+
}
|
|
342
|
+
}}
|
|
343
|
+
/>
|
|
344
|
+
);
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const renderContent = (): React.ReactElement => {
|
|
348
|
+
if (isMobileDevice()) {
|
|
349
|
+
if (mode === "date") {
|
|
350
|
+
return renderDateCalendar();
|
|
351
|
+
} else if (mode === "time") {
|
|
352
|
+
return renderMobileTime();
|
|
353
|
+
} else {
|
|
354
|
+
return renderDateTime();
|
|
355
|
+
}
|
|
356
|
+
} else {
|
|
357
|
+
if (mode === "date") {
|
|
358
|
+
return renderDateCalendar();
|
|
359
|
+
} else if (mode === "time") {
|
|
360
|
+
return renderWebTime();
|
|
361
|
+
} else {
|
|
362
|
+
return renderDateTime();
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
return (
|
|
368
|
+
<Modal
|
|
369
|
+
primaryButtonOnClick={sendOnChange}
|
|
370
|
+
primaryButtonText="Save"
|
|
371
|
+
secondaryButtonOnClick={onDismiss}
|
|
372
|
+
secondaryButtonText="Cancel"
|
|
373
|
+
visible={visible}
|
|
374
|
+
onDismiss={onDismiss}
|
|
375
|
+
>
|
|
376
|
+
{renderContent()}
|
|
377
|
+
</Modal>
|
|
54
378
|
);
|
|
55
379
|
}
|
package/src/Field.tsx
CHANGED
|
@@ -22,6 +22,7 @@ export interface FieldProps extends FieldWithLabelsProps {
|
|
|
22
22
|
| "currency"
|
|
23
23
|
| "customSelect"
|
|
24
24
|
| "date"
|
|
25
|
+
| "datetime"
|
|
25
26
|
| "email"
|
|
26
27
|
| "multiselect"
|
|
27
28
|
| "number"
|
|
@@ -31,6 +32,7 @@ export interface FieldProps extends FieldWithLabelsProps {
|
|
|
31
32
|
| "select"
|
|
32
33
|
| "text"
|
|
33
34
|
| "textarea"
|
|
35
|
+
| "time"
|
|
34
36
|
| "url";
|
|
35
37
|
rows?: number;
|
|
36
38
|
value?: any;
|
|
@@ -149,14 +151,12 @@ export const Field = ({
|
|
|
149
151
|
onChange={(result) => handleSwitchChange(result)}
|
|
150
152
|
/>
|
|
151
153
|
);
|
|
152
|
-
} else if (type
|
|
154
|
+
} else if (type && ["date", "time", "datetime"].includes(type)) {
|
|
153
155
|
return (
|
|
154
156
|
<TextField
|
|
155
|
-
disabled
|
|
156
157
|
id={name}
|
|
157
158
|
placeholder={placeholder}
|
|
158
|
-
type="date"
|
|
159
|
-
// TODO: allow editing with a date picker
|
|
159
|
+
type={type as "date" | "time" | "datetime"}
|
|
160
160
|
value={value}
|
|
161
161
|
onChange={(result) => onChange(result.value)}
|
|
162
162
|
/>
|
|
@@ -230,7 +230,10 @@ export const Field = ({
|
|
|
230
230
|
let tfValue: string = value;
|
|
231
231
|
// Number is supported differently because we need fractional numbers and they don't work
|
|
232
232
|
// well on iOS.
|
|
233
|
-
if (
|
|
233
|
+
if (
|
|
234
|
+
type &&
|
|
235
|
+
["date", "time", "datetime", "email", "phoneNumber", "password", "url"].includes(type)
|
|
236
|
+
) {
|
|
234
237
|
tfType = type as TextFieldType;
|
|
235
238
|
} else if (type === "percent" || type === "currency") {
|
|
236
239
|
tfType = "text";
|
|
@@ -252,7 +255,18 @@ export const Field = ({
|
|
|
252
255
|
disabled={disabled}
|
|
253
256
|
id={name}
|
|
254
257
|
placeholder={placeholder}
|
|
255
|
-
type={
|
|
258
|
+
type={
|
|
259
|
+
tfType as
|
|
260
|
+
| "date"
|
|
261
|
+
| "datetime"
|
|
262
|
+
| "email"
|
|
263
|
+
| "number"
|
|
264
|
+
| "password"
|
|
265
|
+
| "phoneNumber"
|
|
266
|
+
| "text"
|
|
267
|
+
| "time"
|
|
268
|
+
| "url"
|
|
269
|
+
}
|
|
256
270
|
value={tfValue}
|
|
257
271
|
onChange={(result) => onChange(result.value)}
|
|
258
272
|
/>
|