ferns-ui 0.36.4 → 0.36.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.
- package/package.json +3 -4
- package/src/ActionSheet.tsx +1231 -0
- package/src/Avatar.tsx +317 -0
- package/src/Badge.tsx +65 -0
- package/src/Banner.tsx +124 -0
- package/src/BlurBox.native.tsx +40 -0
- package/src/BlurBox.tsx +31 -0
- package/src/Body.tsx +32 -0
- package/src/Box.tsx +308 -0
- package/src/Button.tsx +219 -0
- package/src/Card.tsx +23 -0
- package/src/CheckBox.tsx +118 -0
- package/src/Common.ts +2743 -0
- package/src/Constants.ts +53 -0
- package/src/CustomSelect.tsx +85 -0
- package/src/DateTimeActionSheet.tsx +409 -0
- package/src/DateTimeField.android.tsx +101 -0
- package/src/DateTimeField.ios.tsx +83 -0
- package/src/DateTimeField.tsx +69 -0
- package/src/DecimalRangeActionSheet.tsx +113 -0
- package/src/ErrorBoundary.tsx +37 -0
- package/src/ErrorPage.tsx +44 -0
- package/src/FernsProvider.tsx +21 -0
- package/src/Field.tsx +299 -0
- package/src/FieldWithLabels.tsx +36 -0
- package/src/FlatList.tsx +2 -0
- package/src/Form.tsx +182 -0
- package/src/HeaderButtons.tsx +107 -0
- package/src/Heading.tsx +53 -0
- package/src/HeightActionSheet.tsx +104 -0
- package/src/Hyperlink.tsx +181 -0
- package/src/Icon.tsx +24 -0
- package/src/IconButton.tsx +165 -0
- package/src/Image.tsx +50 -0
- package/src/ImageBackground.tsx +14 -0
- package/src/InfoTooltipButton.tsx +23 -0
- package/src/Layer.tsx +17 -0
- package/src/Link.tsx +17 -0
- package/src/Mask.tsx +21 -0
- package/src/MediaQuery.ts +46 -0
- package/src/Meta.tsx +9 -0
- package/src/Modal.tsx +248 -0
- package/src/ModalSheet.tsx +58 -0
- package/src/NumberPickerActionSheet.tsx +66 -0
- package/src/Page.tsx +133 -0
- package/src/Permissions.ts +44 -0
- package/src/PickerSelect.tsx +553 -0
- package/src/Pill.tsx +24 -0
- package/src/Pog.tsx +87 -0
- package/src/ProgressBar.tsx +55 -0
- package/src/ScrollView.tsx +2 -0
- package/src/SegmentedControl.tsx +102 -0
- package/src/SelectList.tsx +89 -0
- package/src/SideDrawer.tsx +62 -0
- package/src/Spinner.tsx +20 -0
- package/src/SplitPage.native.tsx +160 -0
- package/src/SplitPage.tsx +302 -0
- package/src/Switch.tsx +19 -0
- package/src/Table.tsx +87 -0
- package/src/TableHeader.tsx +36 -0
- package/src/TableHeaderCell.tsx +76 -0
- package/src/TableRow.tsx +87 -0
- package/src/TapToEdit.tsx +221 -0
- package/src/Text.tsx +131 -0
- package/src/TextArea.tsx +16 -0
- package/src/TextField.tsx +401 -0
- package/src/TextFieldNumberActionSheet.tsx +61 -0
- package/src/Toast.tsx +106 -0
- package/src/Tooltip.tsx +269 -0
- package/src/UnifiedScreens.ts +24 -0
- package/src/Unifier.ts +371 -0
- package/src/Utilities.tsx +159 -0
- package/src/WithLabel.tsx +57 -0
- package/src/dayjsExtended.ts +10 -0
- package/src/index.tsx +1346 -0
- package/src/polyfill.d.ts +11 -0
- package/src/tableContext.tsx +80 -0
package/src/Constants.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export const USSTATESLIST = [
|
|
2
|
+
{label: "AL", value: "Alabama"},
|
|
3
|
+
{label: "AK", value: "Alaska"},
|
|
4
|
+
{label: "AZ", value: "Arizona"},
|
|
5
|
+
{label: "AR", value: "Arkansas"},
|
|
6
|
+
{label: "CA", value: "California"},
|
|
7
|
+
{label: "CO", value: "Colorado"},
|
|
8
|
+
{label: "CT", value: "Connecticut"},
|
|
9
|
+
{label: "DE", value: "Delaware"},
|
|
10
|
+
{label: "DC", value: "District Of Columbia"},
|
|
11
|
+
{label: "FL", value: "Florida"},
|
|
12
|
+
{label: "GA", value: "Georgia"},
|
|
13
|
+
{label: "HI", value: "Hawaii"},
|
|
14
|
+
{label: "ID", value: "Idaho"},
|
|
15
|
+
{label: "IL", value: "Illinois"},
|
|
16
|
+
{label: "IN", value: "Indiana"},
|
|
17
|
+
{label: "IA", value: "Iowa"},
|
|
18
|
+
{label: "KS", value: "Kansas"},
|
|
19
|
+
{label: "KY", value: "Kentucky"},
|
|
20
|
+
{label: "LA", value: "Louisiana"},
|
|
21
|
+
{label: "ME", value: "Maine"},
|
|
22
|
+
{label: "MD", value: "Maryland"},
|
|
23
|
+
{label: "MA", value: "Massachusetts"},
|
|
24
|
+
{label: "MI", value: "Michigan"},
|
|
25
|
+
{label: "MN", value: "Minnesota"},
|
|
26
|
+
{label: "MS", value: "Mississippi"},
|
|
27
|
+
{label: "MO", value: "Missouri"},
|
|
28
|
+
{label: "MT", value: "Montana"},
|
|
29
|
+
{label: "NE", value: "Nebraska"},
|
|
30
|
+
{label: "NV", value: "Nevada"},
|
|
31
|
+
{label: "NH", value: "New Hampshire"},
|
|
32
|
+
{label: "NJ", value: "New Jersey"},
|
|
33
|
+
{label: "NM", value: "New Mexico"},
|
|
34
|
+
{label: "NY", value: "New York"},
|
|
35
|
+
{label: "NC", value: "North Carolina"},
|
|
36
|
+
{label: "ND", value: "North Dakota"},
|
|
37
|
+
{label: "OH", value: "Ohio"},
|
|
38
|
+
{label: "OK", value: "Oklahoma"},
|
|
39
|
+
{label: "OR", value: "Oregon"},
|
|
40
|
+
{label: "PA", value: "Pennsylvania"},
|
|
41
|
+
{label: "RI", value: "Rhode Island"},
|
|
42
|
+
{label: "SC", value: "South Carolina"},
|
|
43
|
+
{label: "SD", value: "South Dakota"},
|
|
44
|
+
{label: "TN", value: "Tennessee"},
|
|
45
|
+
{label: "TX", value: "Texas"},
|
|
46
|
+
{label: "UT", value: "Utah"},
|
|
47
|
+
{label: "VT", value: "Vermont"},
|
|
48
|
+
{label: "VA", value: "Virginia"},
|
|
49
|
+
{label: "WA", value: "Washington"},
|
|
50
|
+
{label: "WV", value: "West Virginia"},
|
|
51
|
+
{label: "WI", value: "Wisconsin"},
|
|
52
|
+
{label: "WY", value: "Wyoming"},
|
|
53
|
+
];
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import React, {ReactElement, useEffect, useMemo, useState} from "react";
|
|
2
|
+
|
|
3
|
+
import {Box} from "./Box";
|
|
4
|
+
import {SelectList} from "./SelectList";
|
|
5
|
+
import {TextField} from "./TextField";
|
|
6
|
+
|
|
7
|
+
export interface CustomSelectProps {
|
|
8
|
+
value: string;
|
|
9
|
+
onChange: (value: string) => void;
|
|
10
|
+
options: Array<{label: string; value: string}>;
|
|
11
|
+
placeholder?: string;
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
label?: string;
|
|
14
|
+
labelColor?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const CustomSelect = ({
|
|
18
|
+
value,
|
|
19
|
+
onChange,
|
|
20
|
+
placeholder,
|
|
21
|
+
disabled,
|
|
22
|
+
options,
|
|
23
|
+
}: CustomSelectProps): ReactElement | null => {
|
|
24
|
+
const [customValue, setCustomValue] = useState(value);
|
|
25
|
+
const [showCustomInput, setShowCustomInput] = useState(false);
|
|
26
|
+
|
|
27
|
+
// Boolean that checks if customValue is a value from the
|
|
28
|
+
// options prop or if it is a true custom value
|
|
29
|
+
const isValueCustom: boolean = useMemo((): boolean => {
|
|
30
|
+
// We add an empty value to protect against an empty string custom value or if the placeholder value is selected
|
|
31
|
+
return ![...options, {value: ""}].map((i) => i.value).includes(customValue);
|
|
32
|
+
}, [options, customValue]);
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
setShowCustomInput(isValueCustom);
|
|
36
|
+
if (!showCustomInput) {
|
|
37
|
+
setCustomValue(value);
|
|
38
|
+
}
|
|
39
|
+
}, [showCustomInput, value, isValueCustom]);
|
|
40
|
+
|
|
41
|
+
// Custom select has 3 values - the overall field value, the value of the select menu, and the value of the custom input
|
|
42
|
+
const handleCustomSelectListChange = (newValue: string) => {
|
|
43
|
+
// If "custom" is selected from the dropdown, toggle the custom input open and clear the previous value
|
|
44
|
+
if (newValue === "custom") {
|
|
45
|
+
setShowCustomInput(true);
|
|
46
|
+
setCustomValue(isValueCustom ? "custom" : newValue);
|
|
47
|
+
onChange("");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// If any non-custom value is selected
|
|
51
|
+
else {
|
|
52
|
+
// Close the custom input if open and clear the value
|
|
53
|
+
if (showCustomInput) {
|
|
54
|
+
setShowCustomInput(false);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Update the field value and select value
|
|
58
|
+
onChange(newValue);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<>
|
|
64
|
+
<SelectList
|
|
65
|
+
id="providedOptions"
|
|
66
|
+
options={[...options, {label: "Custom", value: "custom"}]}
|
|
67
|
+
placeholder={placeholder}
|
|
68
|
+
value={isValueCustom ? "custom" : customValue}
|
|
69
|
+
onChange={handleCustomSelectListChange}
|
|
70
|
+
/>
|
|
71
|
+
{Boolean(showCustomInput) && (
|
|
72
|
+
<Box paddingY={2}>
|
|
73
|
+
<TextField
|
|
74
|
+
disabled={disabled}
|
|
75
|
+
id="customOptions"
|
|
76
|
+
placeholder={placeholder}
|
|
77
|
+
type="text"
|
|
78
|
+
value={value}
|
|
79
|
+
onChange={(result) => onChange(result.value)}
|
|
80
|
+
/>
|
|
81
|
+
</Box>
|
|
82
|
+
)}
|
|
83
|
+
</>
|
|
84
|
+
);
|
|
85
|
+
};
|
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
import {Picker} from "@react-native-picker/picker";
|
|
2
|
+
import range from "lodash/range";
|
|
3
|
+
import React, {useEffect, useState} from "react";
|
|
4
|
+
import {Platform, StyleProp, TextInput, TextStyle, View} from "react-native";
|
|
5
|
+
import {Calendar} from "react-native-calendars";
|
|
6
|
+
|
|
7
|
+
import {Box} from "./Box";
|
|
8
|
+
import {OnChangeCallback} from "./Common";
|
|
9
|
+
import dayjs from "./dayjsExtended";
|
|
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 = dayjs(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
|
+
}
|
|
155
|
+
|
|
156
|
+
interface DateTimeActionSheetProps {
|
|
157
|
+
value?: string;
|
|
158
|
+
mode?: "date" | "time" | "datetime";
|
|
159
|
+
// Returns an ISO 8601 string. If mode is "time", the date portion is today.
|
|
160
|
+
onChange: OnChangeCallback;
|
|
161
|
+
actionSheetRef: React.RefObject<any>;
|
|
162
|
+
visible: boolean;
|
|
163
|
+
onDismiss: () => void;
|
|
164
|
+
}
|
|
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.
|
|
174
|
+
export function DateTimeActionSheet({
|
|
175
|
+
// actionSheetRef,
|
|
176
|
+
mode,
|
|
177
|
+
value,
|
|
178
|
+
onChange,
|
|
179
|
+
visible,
|
|
180
|
+
onDismiss,
|
|
181
|
+
}: DateTimeActionSheetProps) {
|
|
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
|
+
let m;
|
|
184
|
+
if (value) {
|
|
185
|
+
m = dayjs(value, ["YYYY", "YYYY-MM-DD", "HH:mm", "hh:mm A"]);
|
|
186
|
+
} else {
|
|
187
|
+
m = dayjs();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (!m.isValid()) {
|
|
191
|
+
throw new Error(`Invalid date/time value ${value}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
let hr = dayjs(m).hour() % 12;
|
|
195
|
+
if (hr === 0) {
|
|
196
|
+
hr = 12;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const [hour, setHour] = useState<number>(hr);
|
|
200
|
+
const [minute, setMinute] = useState<number>(dayjs(m).minute());
|
|
201
|
+
const [amPm, setAmPm] = useState<"am" | "pm">(dayjs(m).format("a") === "am" ? "am" : "pm");
|
|
202
|
+
const [date, setDate] = useState<string>(dayjs(m).toISOString());
|
|
203
|
+
|
|
204
|
+
useEffect(() => {
|
|
205
|
+
let datetime;
|
|
206
|
+
if (value) {
|
|
207
|
+
datetime = dayjs(value, ["YYYY", "YYYY-MM-DD", "HH:mm", "hh:mm A"]);
|
|
208
|
+
} else {
|
|
209
|
+
datetime = dayjs();
|
|
210
|
+
}
|
|
211
|
+
let h = dayjs(datetime).hour() % 12;
|
|
212
|
+
if (h === 0) {
|
|
213
|
+
h = 12;
|
|
214
|
+
}
|
|
215
|
+
setHour(h);
|
|
216
|
+
setMinute(dayjs(datetime).minute());
|
|
217
|
+
setAmPm(dayjs(datetime).format("a") === "am" ? "am" : "pm");
|
|
218
|
+
setDate(dayjs(datetime).toISOString());
|
|
219
|
+
}, [value]);
|
|
220
|
+
|
|
221
|
+
// TODO Support 24 hour time for time picker.
|
|
222
|
+
const renderMobileTime = () => {
|
|
223
|
+
return (
|
|
224
|
+
<Box direction="row" width="100%">
|
|
225
|
+
<Box paddingY={2} width="35%">
|
|
226
|
+
<Picker
|
|
227
|
+
itemStyle={{
|
|
228
|
+
height: TIME_PICKER_HEIGHT,
|
|
229
|
+
}}
|
|
230
|
+
selectedValue={hour}
|
|
231
|
+
style={{
|
|
232
|
+
height: TIME_PICKER_HEIGHT,
|
|
233
|
+
backgroundColor: "#FFFFFF",
|
|
234
|
+
}}
|
|
235
|
+
onValueChange={(itemValue) => setHour(itemValue)}
|
|
236
|
+
>
|
|
237
|
+
{hours.map((n) => (
|
|
238
|
+
<Picker.Item key={String(n)} label={String(n)} value={String(n)} />
|
|
239
|
+
))}
|
|
240
|
+
</Picker>
|
|
241
|
+
</Box>
|
|
242
|
+
<Box paddingY={2} width="35%">
|
|
243
|
+
<Picker
|
|
244
|
+
itemStyle={{
|
|
245
|
+
height: TIME_PICKER_HEIGHT,
|
|
246
|
+
}}
|
|
247
|
+
selectedValue={minute}
|
|
248
|
+
style={{
|
|
249
|
+
height: TIME_PICKER_HEIGHT,
|
|
250
|
+
backgroundColor: "#FFFFFF",
|
|
251
|
+
}}
|
|
252
|
+
onValueChange={(itemValue) => setMinute(itemValue)}
|
|
253
|
+
>
|
|
254
|
+
{minutes.map((n) => (
|
|
255
|
+
<Picker.Item key={String(n)} label={String(n)} value={String(n)} />
|
|
256
|
+
))}
|
|
257
|
+
</Picker>
|
|
258
|
+
</Box>
|
|
259
|
+
<Box paddingY={2} width="30%">
|
|
260
|
+
<Picker
|
|
261
|
+
itemStyle={{
|
|
262
|
+
height: TIME_PICKER_HEIGHT,
|
|
263
|
+
}}
|
|
264
|
+
selectedValue={amPm}
|
|
265
|
+
style={{
|
|
266
|
+
height: TIME_PICKER_HEIGHT,
|
|
267
|
+
backgroundColor: "#FFFFFF",
|
|
268
|
+
}}
|
|
269
|
+
onValueChange={(itemValue) => setAmPm(itemValue)}
|
|
270
|
+
>
|
|
271
|
+
<Picker.Item key="am" label="am" value="am" />
|
|
272
|
+
<Picker.Item key="pm" label="pm" value="pm" />
|
|
273
|
+
</Picker>
|
|
274
|
+
</Box>
|
|
275
|
+
</Box>
|
|
276
|
+
);
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
// TODO: Support a typeahead dropdown for time picker, similar to Google Calendar on the web.
|
|
280
|
+
const renderWebTime = () => {
|
|
281
|
+
return (
|
|
282
|
+
<Box direction="row" justifyContent="center" width="100%">
|
|
283
|
+
<Box width={60}>
|
|
284
|
+
<TimeInput type="hour" value={hour} onChange={(v) => setHour(v)} />
|
|
285
|
+
</Box>
|
|
286
|
+
<Box
|
|
287
|
+
alignItems="center"
|
|
288
|
+
height={INPUT_HEIGHT}
|
|
289
|
+
justifyContent="center"
|
|
290
|
+
marginLeft={2}
|
|
291
|
+
marginRight={2}
|
|
292
|
+
>
|
|
293
|
+
<Heading size="md">:</Heading>
|
|
294
|
+
</Box>
|
|
295
|
+
<Box marginRight={2} width={60}>
|
|
296
|
+
<TimeInput type="minute" value={minute} onChange={(v) => setMinute(v)} />
|
|
297
|
+
</Box>
|
|
298
|
+
|
|
299
|
+
<Box width={60}>
|
|
300
|
+
<SelectList
|
|
301
|
+
options={[
|
|
302
|
+
{label: "am", value: "am"},
|
|
303
|
+
{label: "pm", value: "pm"},
|
|
304
|
+
]}
|
|
305
|
+
style={{minHeight: INPUT_HEIGHT}}
|
|
306
|
+
value={amPm}
|
|
307
|
+
onChange={(result) => {
|
|
308
|
+
setAmPm(result as "am" | "pm");
|
|
309
|
+
}}
|
|
310
|
+
/>
|
|
311
|
+
</Box>
|
|
312
|
+
</Box>
|
|
313
|
+
);
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const renderDateTime = (): React.ReactElement => {
|
|
317
|
+
return (
|
|
318
|
+
<Box>
|
|
319
|
+
<Box marginBottom={2}>{renderDateCalendar()}</Box>
|
|
320
|
+
{isMobileDevice() ? renderMobileTime() : renderWebTime()}
|
|
321
|
+
</Box>
|
|
322
|
+
);
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
// Note: do not call this if waiting on a state change.
|
|
326
|
+
const sendOnChange = () => {
|
|
327
|
+
const hourChange = amPm === "pm" && hour !== 12 ? Number(hour) + 12 : Number(hour);
|
|
328
|
+
if (mode === "date") {
|
|
329
|
+
onChange({value: date});
|
|
330
|
+
} else if (mode === "time") {
|
|
331
|
+
onChange({
|
|
332
|
+
value: dayjs().hour(hourChange).minute(Number(minute)).toISOString(),
|
|
333
|
+
});
|
|
334
|
+
} else if (mode === "datetime") {
|
|
335
|
+
onChange({
|
|
336
|
+
value: dayjs(date).hour(hourChange).minute(Number(minute)).toISOString(),
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
onDismiss();
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const sendClear = () => {
|
|
343
|
+
onChange({
|
|
344
|
+
value: "",
|
|
345
|
+
});
|
|
346
|
+
onDismiss();
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
// Renders our custom calendar component on mobile or web.
|
|
350
|
+
const renderDateCalendar = () => {
|
|
351
|
+
const markedDates: {[id: string]: {selected: boolean; selectedColor: string}} = {};
|
|
352
|
+
if (date) {
|
|
353
|
+
markedDates[dayjs(date).format("YYYY-MM-DD")] = {
|
|
354
|
+
selected: true,
|
|
355
|
+
selectedColor: Unifier.theme.primary,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
return (
|
|
359
|
+
<Calendar
|
|
360
|
+
customHeader={CalendarHeader}
|
|
361
|
+
initialDate={dayjs(date).format("YYYY-MM-DD")}
|
|
362
|
+
markedDates={markedDates}
|
|
363
|
+
onDayPress={(day) => {
|
|
364
|
+
setDate(day.dateString);
|
|
365
|
+
// If mode is just date, we can shortcut and close right away. time and datetime need to wait for the
|
|
366
|
+
// primary button.
|
|
367
|
+
if (mode === "date") {
|
|
368
|
+
onChange({value: day.dateString});
|
|
369
|
+
onDismiss();
|
|
370
|
+
}
|
|
371
|
+
}}
|
|
372
|
+
/>
|
|
373
|
+
);
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
const renderContent = (): React.ReactElement => {
|
|
377
|
+
if (isMobileDevice()) {
|
|
378
|
+
if (mode === "date") {
|
|
379
|
+
return renderDateCalendar();
|
|
380
|
+
} else if (mode === "time") {
|
|
381
|
+
return renderMobileTime();
|
|
382
|
+
} else {
|
|
383
|
+
return renderDateTime();
|
|
384
|
+
}
|
|
385
|
+
} else {
|
|
386
|
+
if (mode === "date") {
|
|
387
|
+
return renderDateCalendar();
|
|
388
|
+
} else if (mode === "time") {
|
|
389
|
+
return renderWebTime();
|
|
390
|
+
} else {
|
|
391
|
+
return renderDateTime();
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
return (
|
|
397
|
+
<Modal
|
|
398
|
+
primaryButtonOnClick={sendOnChange}
|
|
399
|
+
primaryButtonText="Save"
|
|
400
|
+
secondaryButtonOnClick={sendClear}
|
|
401
|
+
secondaryButtonText="Clear"
|
|
402
|
+
showClose
|
|
403
|
+
visible={visible}
|
|
404
|
+
onDismiss={onDismiss}
|
|
405
|
+
>
|
|
406
|
+
{renderContent()}
|
|
407
|
+
</Modal>
|
|
408
|
+
);
|
|
409
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import DateTimePicker from "@react-native-community/datetimepicker";
|
|
2
|
+
import React, {ReactElement, useMemo, useState} from "react";
|
|
3
|
+
import {TextInput} from "react-native";
|
|
4
|
+
|
|
5
|
+
import {DateTimeFieldProps} from "./Common";
|
|
6
|
+
import dayjs from "./dayjsExtended";
|
|
7
|
+
import {Unifier} from "./Unifier";
|
|
8
|
+
import {WithLabel} from "./WithLabel";
|
|
9
|
+
|
|
10
|
+
export const DateTimeField = ({
|
|
11
|
+
mode,
|
|
12
|
+
value,
|
|
13
|
+
onChange,
|
|
14
|
+
errorMessage,
|
|
15
|
+
pickerType = "default",
|
|
16
|
+
dateFormat,
|
|
17
|
+
errorMessageColor,
|
|
18
|
+
}: DateTimeFieldProps): ReactElement => {
|
|
19
|
+
// const [showCalendar, setShowCalendar] = useState(false);
|
|
20
|
+
// const [showClock, setShowClock] = useState(false);
|
|
21
|
+
// const [tempDate, setTempDate] = useState<Date>();
|
|
22
|
+
const [pickerMode, setPickerMode] = useState(mode);
|
|
23
|
+
const [showPicker, setShowPicker] = useState(false);
|
|
24
|
+
|
|
25
|
+
const showCalendarFirst = mode === "datetime" || mode === "date";
|
|
26
|
+
|
|
27
|
+
const defaultFormat = useMemo(() => {
|
|
28
|
+
if (dateFormat) {
|
|
29
|
+
return dateFormat;
|
|
30
|
+
} else {
|
|
31
|
+
if (mode === "date") {
|
|
32
|
+
return "MMMM Do YYYY";
|
|
33
|
+
} else if (mode === "time") {
|
|
34
|
+
return "h:mm a";
|
|
35
|
+
} else {
|
|
36
|
+
return "MMMM Do YYYY, h:mm a";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}, [mode, dateFormat]);
|
|
40
|
+
|
|
41
|
+
const showMode = (currentMode: "date" | "time") => {
|
|
42
|
+
setShowPicker(true);
|
|
43
|
+
setPickerMode(currentMode);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const showDatePicker = () => {
|
|
47
|
+
showMode("date");
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const showTimePicker = () => {
|
|
51
|
+
showMode("time");
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<WithLabel
|
|
56
|
+
label={errorMessage}
|
|
57
|
+
labelColor={errorMessageColor || "red"}
|
|
58
|
+
labelPlacement="after"
|
|
59
|
+
labelSize="sm"
|
|
60
|
+
>
|
|
61
|
+
<WithLabel>
|
|
62
|
+
<TextInput
|
|
63
|
+
inputMode="none"
|
|
64
|
+
style={{
|
|
65
|
+
flex: 1,
|
|
66
|
+
paddingTop: 10,
|
|
67
|
+
paddingRight: 10,
|
|
68
|
+
paddingBottom: 10,
|
|
69
|
+
paddingLeft: 10,
|
|
70
|
+
height: 40,
|
|
71
|
+
width: "100%",
|
|
72
|
+
color: Unifier.theme.darkGray,
|
|
73
|
+
fontFamily: Unifier.theme.primaryFont,
|
|
74
|
+
borderWidth: 1,
|
|
75
|
+
}}
|
|
76
|
+
value={dayjs(value).format(defaultFormat)}
|
|
77
|
+
onPressIn={() => {
|
|
78
|
+
showCalendarFirst ? showDatePicker() : showTimePicker();
|
|
79
|
+
}}
|
|
80
|
+
/>
|
|
81
|
+
{showPicker && (
|
|
82
|
+
<DateTimePicker
|
|
83
|
+
display={pickerType}
|
|
84
|
+
mode={pickerMode}
|
|
85
|
+
testID="dateTimePicker"
|
|
86
|
+
value={value}
|
|
87
|
+
onChange={(event, date) => {
|
|
88
|
+
if (date) {
|
|
89
|
+
onChange(date);
|
|
90
|
+
if (pickerMode === "date" && mode === "datetime") {
|
|
91
|
+
showTimePicker();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
setShowPicker(false);
|
|
95
|
+
}}
|
|
96
|
+
/>
|
|
97
|
+
)}
|
|
98
|
+
</WithLabel>
|
|
99
|
+
</WithLabel>
|
|
100
|
+
);
|
|
101
|
+
};
|