@oneuptime/common 8.0.5289 → 8.0.5300
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/Tests/UI/Components/TimePicker/TimePicker.test.tsx +535 -0
- package/UI/Components/DuplicateModel/DuplicateModel.tsx +4 -2
- package/UI/Components/Forms/Fields/FormField.tsx +26 -0
- package/UI/Components/Input/Input.tsx +4 -21
- package/UI/Components/TimePicker/Index.ts +3 -0
- package/UI/Components/TimePicker/TimePicker.tsx +476 -0
- package/build/dist/Tests/UI/Components/TimePicker/TimePicker.test.js +358 -0
- package/build/dist/Tests/UI/Components/TimePicker/TimePicker.test.js.map +1 -0
- package/build/dist/UI/Components/DuplicateModel/DuplicateModel.js +3 -2
- package/build/dist/UI/Components/DuplicateModel/DuplicateModel.js.map +1 -1
- package/build/dist/UI/Components/Forms/Fields/FormField.js +10 -0
- package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
- package/build/dist/UI/Components/Input/Input.js +4 -23
- package/build/dist/UI/Components/Input/Input.js.map +1 -1
- package/build/dist/UI/Components/TimePicker/Index.js +3 -0
- package/build/dist/UI/Components/TimePicker/Index.js.map +1 -0
- package/build/dist/UI/Components/TimePicker/TimePicker.js +218 -0
- package/build/dist/UI/Components/TimePicker/TimePicker.js.map +1 -0
- package/jest.config.json +6 -2
- package/package.json +1 -1
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
FunctionComponent,
|
|
3
|
+
ReactElement,
|
|
4
|
+
useEffect,
|
|
5
|
+
useMemo,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
} from "react";
|
|
9
|
+
import OneUptimeDate from "../../../Types/Date";
|
|
10
|
+
import Icon from "../Icon/Icon";
|
|
11
|
+
import IconProp from "../../../Types/Icon/IconProp";
|
|
12
|
+
import Modal, { ModalWidth } from "../Modal/Modal";
|
|
13
|
+
|
|
14
|
+
export interface ComponentProps {
|
|
15
|
+
value?: string | Date | undefined; // ISO string or Date
|
|
16
|
+
onChange?: undefined | ((value: string) => void); // emits ISO string
|
|
17
|
+
placeholder?: string | undefined;
|
|
18
|
+
className?: string | undefined;
|
|
19
|
+
readOnly?: boolean | undefined;
|
|
20
|
+
disabled?: boolean | undefined;
|
|
21
|
+
onFocus?: (() => void) | undefined;
|
|
22
|
+
onBlur?: (() => void) | undefined;
|
|
23
|
+
dataTestId?: string | undefined;
|
|
24
|
+
tabIndex?: number | undefined;
|
|
25
|
+
autoFocus?: boolean | undefined;
|
|
26
|
+
error?: string | undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const pad2: (n: number) => string = (n: number): string => {
|
|
30
|
+
return n < 10 ? `0${n}` : `${n}`;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const clamp: (n: number, min: number, max: number) => number = (
|
|
34
|
+
n: number,
|
|
35
|
+
min: number,
|
|
36
|
+
max: number,
|
|
37
|
+
): number => {
|
|
38
|
+
return Math.min(Math.max(n, min), max);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const toDate: (v?: string | Date) => Date | undefined = (
|
|
42
|
+
v?: string | Date,
|
|
43
|
+
): Date | undefined => {
|
|
44
|
+
if (!v) {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
return OneUptimeDate.fromString(v);
|
|
49
|
+
} catch {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const TimePicker: FunctionComponent<ComponentProps> = (
|
|
55
|
+
props: ComponentProps,
|
|
56
|
+
): ReactElement => {
|
|
57
|
+
// Start with project-level preference (works on server), then update to browser preference on mount
|
|
58
|
+
const [userPrefers12h, setUserPrefers12h] = useState<boolean>(
|
|
59
|
+
OneUptimeDate.getUserPrefers12HourFormat(),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
useEffect((): void => {
|
|
63
|
+
// Resolve to actual browser preference once mounted using project utility
|
|
64
|
+
setUserPrefers12h(OneUptimeDate.getUserPrefers12HourFormat());
|
|
65
|
+
}, []);
|
|
66
|
+
|
|
67
|
+
// Timezone label derived from OneUptimeDate utilities (e.g., "PDT (America/Los_Angeles)" or "GMT+5:30 (Asia/Kolkata)")
|
|
68
|
+
const [timezoneLabel, setTimezoneLabel] = useState<string>(
|
|
69
|
+
"your local time zone",
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
useEffect((): void => {
|
|
73
|
+
const abbr: string = OneUptimeDate.getCurrentTimezoneString();
|
|
74
|
+
const iana: string =
|
|
75
|
+
OneUptimeDate.getCurrentTimezone() as unknown as string;
|
|
76
|
+
setTimezoneLabel(`${abbr}${iana ? ` (${iana})` : ""}`);
|
|
77
|
+
}, []);
|
|
78
|
+
|
|
79
|
+
const initialDate: Date = useMemo(() => {
|
|
80
|
+
return toDate(props.value) || OneUptimeDate.getCurrentDate();
|
|
81
|
+
}, [props.value]);
|
|
82
|
+
|
|
83
|
+
const [hours24, setHours24] = useState<number>(initialDate.getHours());
|
|
84
|
+
const [minutes, setMinutes] = useState<number>(initialDate.getMinutes());
|
|
85
|
+
|
|
86
|
+
const hoursInputRef: React.MutableRefObject<HTMLInputElement | null> =
|
|
87
|
+
useRef<HTMLInputElement | null>(null);
|
|
88
|
+
const minutesInputRef: React.MutableRefObject<HTMLInputElement | null> =
|
|
89
|
+
useRef<HTMLInputElement | null>(null);
|
|
90
|
+
|
|
91
|
+
// Modal state
|
|
92
|
+
const [showModal, setShowModal] = useState<boolean>(false);
|
|
93
|
+
const [tempHours24, setTempHours24] = useState<number>(hours24);
|
|
94
|
+
const [tempMinutes, setTempMinutes] = useState<number>(minutes);
|
|
95
|
+
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
const d: Date | undefined = toDate(props.value);
|
|
98
|
+
if (!d) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
setHours24(d.getHours());
|
|
102
|
+
setMinutes(d.getMinutes());
|
|
103
|
+
}, [props.value]);
|
|
104
|
+
|
|
105
|
+
const emitChange: (h24: number, m: number) => void = (
|
|
106
|
+
h24: number,
|
|
107
|
+
m: number,
|
|
108
|
+
): void => {
|
|
109
|
+
const date: Date = OneUptimeDate.getDateWithCustomTime({
|
|
110
|
+
hours: clamp(h24, 0, 23),
|
|
111
|
+
minutes: clamp(m, 0, 59),
|
|
112
|
+
seconds: 0,
|
|
113
|
+
});
|
|
114
|
+
props.onChange?.(OneUptimeDate.toString(date));
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const display: { hours: string; minutes: string; isPM: boolean } =
|
|
118
|
+
useMemo(() => {
|
|
119
|
+
if (userPrefers12h) {
|
|
120
|
+
const isPM: boolean = hours24 >= 12;
|
|
121
|
+
const hr12: number = ((hours24 + 11) % 12) + 1; // 0->12
|
|
122
|
+
return { hours: pad2(hr12), minutes: pad2(minutes), isPM };
|
|
123
|
+
}
|
|
124
|
+
return { hours: pad2(hours24), minutes: pad2(minutes), isPM: false };
|
|
125
|
+
}, [hours24, minutes, userPrefers12h]);
|
|
126
|
+
|
|
127
|
+
// Inline editing disabled; all updates happen inside modal
|
|
128
|
+
|
|
129
|
+
const clickable: boolean = !(props.readOnly || props.disabled);
|
|
130
|
+
|
|
131
|
+
const baseClass: string =
|
|
132
|
+
props.className ||
|
|
133
|
+
"flex items-center w-full rounded-md border border-gray-300 bg-white text-sm focus-within:border-indigo-500 focus-within:ring-1 focus-within:ring-indigo-500 shadow-sm";
|
|
134
|
+
|
|
135
|
+
const inputClass: string =
|
|
136
|
+
"w-14 text-center py-2 outline-none bg-transparent leading-snug focus:outline-none" +
|
|
137
|
+
(props.readOnly || props.disabled ? " text-gray-500" : " text-gray-900");
|
|
138
|
+
|
|
139
|
+
const openModal: () => void = (): void => {
|
|
140
|
+
if (!clickable) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
setTempHours24(hours24);
|
|
144
|
+
setTempMinutes(minutes);
|
|
145
|
+
setShowModal(true);
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<>
|
|
150
|
+
<div
|
|
151
|
+
className={
|
|
152
|
+
(props.error
|
|
153
|
+
? baseClass +
|
|
154
|
+
" border-red-300 focus-within:border-red-500 focus-within:ring-red-500"
|
|
155
|
+
: baseClass) +
|
|
156
|
+
(props.disabled ? " bg-gray-100" : "") +
|
|
157
|
+
(clickable
|
|
158
|
+
? " cursor-pointer hover:bg-gray-50"
|
|
159
|
+
: " cursor-not-allowed") +
|
|
160
|
+
" mt-2"
|
|
161
|
+
}
|
|
162
|
+
role="group"
|
|
163
|
+
aria-label="Time input"
|
|
164
|
+
aria-disabled={props.disabled ? true : undefined}
|
|
165
|
+
aria-describedby={props.error ? "timepicker-error" : undefined}
|
|
166
|
+
onClick={openModal}
|
|
167
|
+
aria-haspopup="dialog"
|
|
168
|
+
aria-expanded={showModal || undefined}
|
|
169
|
+
>
|
|
170
|
+
{/* Left time icon */}
|
|
171
|
+
<div className="pl-2 pr-2 text-gray-400" aria-hidden="true">
|
|
172
|
+
<Icon icon={IconProp.Time} className="h-5 w-5" />
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
{/* Hours */}
|
|
176
|
+
<input
|
|
177
|
+
ref={hoursInputRef}
|
|
178
|
+
data-testid={props.dataTestId}
|
|
179
|
+
type="text"
|
|
180
|
+
inputMode="numeric"
|
|
181
|
+
pattern="[0-9]*"
|
|
182
|
+
autoFocus={props.autoFocus}
|
|
183
|
+
tabIndex={props.tabIndex}
|
|
184
|
+
spellCheck={false}
|
|
185
|
+
placeholder={props.placeholder || (userPrefers12h ? "hh" : "HH")}
|
|
186
|
+
className={
|
|
187
|
+
inputClass +
|
|
188
|
+
" rounded-l-md pl-1 focus:ring-0 focus-visible:outline-none"
|
|
189
|
+
}
|
|
190
|
+
readOnly={true}
|
|
191
|
+
aria-label="Hours"
|
|
192
|
+
aria-invalid={props.error ? true : undefined}
|
|
193
|
+
value={display.hours}
|
|
194
|
+
onFocus={props.onFocus}
|
|
195
|
+
onBlur={() => {
|
|
196
|
+
return props.onBlur?.();
|
|
197
|
+
}}
|
|
198
|
+
/>
|
|
199
|
+
|
|
200
|
+
<span className="px-2 text-gray-500">:</span>
|
|
201
|
+
|
|
202
|
+
{/* Minutes */}
|
|
203
|
+
<input
|
|
204
|
+
ref={minutesInputRef}
|
|
205
|
+
type="text"
|
|
206
|
+
inputMode="numeric"
|
|
207
|
+
pattern="[0-9]*"
|
|
208
|
+
spellCheck={false}
|
|
209
|
+
placeholder="mm"
|
|
210
|
+
className={
|
|
211
|
+
inputClass +
|
|
212
|
+
" rounded-r-md pr-2 focus:ring-0 focus-visible:outline-none"
|
|
213
|
+
}
|
|
214
|
+
readOnly={true}
|
|
215
|
+
aria-label="Minutes"
|
|
216
|
+
aria-invalid={props.error ? true : undefined}
|
|
217
|
+
value={display.minutes}
|
|
218
|
+
onBlur={() => {
|
|
219
|
+
return props.onBlur?.();
|
|
220
|
+
}}
|
|
221
|
+
/>
|
|
222
|
+
|
|
223
|
+
{/* spacer to maintain layout without right icon */}
|
|
224
|
+
<div className="ml-auto mr-1" />
|
|
225
|
+
|
|
226
|
+
{userPrefers12h && (
|
|
227
|
+
<div className="border-l border-gray-200 pl-2 pr-2 ml-1 mr-1">
|
|
228
|
+
<div
|
|
229
|
+
className="flex items-center gap-1"
|
|
230
|
+
role="group"
|
|
231
|
+
aria-label="AM or PM"
|
|
232
|
+
>
|
|
233
|
+
<button
|
|
234
|
+
type="button"
|
|
235
|
+
className={
|
|
236
|
+
"px-2 py-1 rounded text-xs " +
|
|
237
|
+
(display.isPM
|
|
238
|
+
? "bg-white text-gray-700 border border-gray-300"
|
|
239
|
+
: "bg-indigo-50 text-indigo-700 border border-indigo-200")
|
|
240
|
+
}
|
|
241
|
+
disabled={!clickable}
|
|
242
|
+
aria-pressed={!display.isPM}
|
|
243
|
+
aria-label="Open time selector for AM/PM"
|
|
244
|
+
onClick={openModal}
|
|
245
|
+
>
|
|
246
|
+
AM
|
|
247
|
+
</button>
|
|
248
|
+
<button
|
|
249
|
+
type="button"
|
|
250
|
+
className={
|
|
251
|
+
"px-2 py-1 rounded text-xs " +
|
|
252
|
+
(display.isPM
|
|
253
|
+
? "bg-indigo-50 text-indigo-700 border border-indigo-200"
|
|
254
|
+
: "bg-white text-gray-700 border border-gray-300")
|
|
255
|
+
}
|
|
256
|
+
disabled={!clickable}
|
|
257
|
+
aria-pressed={display.isPM}
|
|
258
|
+
aria-label="Open time selector for AM/PM"
|
|
259
|
+
onClick={openModal}
|
|
260
|
+
>
|
|
261
|
+
PM
|
|
262
|
+
</button>
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
)}
|
|
266
|
+
|
|
267
|
+
{props.error && (
|
|
268
|
+
<div className="pointer-events-none flex items-center pr-3">
|
|
269
|
+
<Icon icon={IconProp.ErrorSolid} className="h-5 w-5 text-red-500" />
|
|
270
|
+
</div>
|
|
271
|
+
)}
|
|
272
|
+
</div>
|
|
273
|
+
|
|
274
|
+
{/* Time picker modal */}
|
|
275
|
+
{showModal && (
|
|
276
|
+
<Modal
|
|
277
|
+
title="Select time"
|
|
278
|
+
description={
|
|
279
|
+
userPrefers12h
|
|
280
|
+
? "Choose hours, minutes, and AM/PM"
|
|
281
|
+
: "Choose hours and minutes"
|
|
282
|
+
}
|
|
283
|
+
modalWidth={ModalWidth.Medium}
|
|
284
|
+
onClose={() => {
|
|
285
|
+
return setShowModal(false);
|
|
286
|
+
}}
|
|
287
|
+
onSubmit={() => {
|
|
288
|
+
setHours24(tempHours24);
|
|
289
|
+
setMinutes(tempMinutes);
|
|
290
|
+
emitChange(tempHours24, tempMinutes);
|
|
291
|
+
setShowModal(false);
|
|
292
|
+
}}
|
|
293
|
+
submitButtonText="Apply"
|
|
294
|
+
>
|
|
295
|
+
<div className="p-2">
|
|
296
|
+
<div className="flex items-center justify-center gap-6">
|
|
297
|
+
{/* Hours selector */}
|
|
298
|
+
<div className="flex flex-col items-center">
|
|
299
|
+
<button
|
|
300
|
+
type="button"
|
|
301
|
+
aria-label="Increase hours"
|
|
302
|
+
className="p-2 rounded hover:bg-gray-50"
|
|
303
|
+
onClick={() => {
|
|
304
|
+
return setTempHours24((h: number): number => {
|
|
305
|
+
return (h + 1) % 24;
|
|
306
|
+
});
|
|
307
|
+
}}
|
|
308
|
+
>
|
|
309
|
+
<Icon icon={IconProp.ChevronUp} className="h-6 w-6" />
|
|
310
|
+
</button>
|
|
311
|
+
<input
|
|
312
|
+
type="text"
|
|
313
|
+
inputMode="numeric"
|
|
314
|
+
pattern="[0-9]*"
|
|
315
|
+
aria-label="Hours"
|
|
316
|
+
className="w-20 text-center text-3xl font-semibold py-2 rounded border border-gray-200 focus:ring-2 focus:ring-indigo-500"
|
|
317
|
+
value={
|
|
318
|
+
userPrefers12h
|
|
319
|
+
? pad2(((tempHours24 + 11) % 12) + 1)
|
|
320
|
+
: pad2(tempHours24)
|
|
321
|
+
}
|
|
322
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement>): void => {
|
|
323
|
+
const raw: string = e.target.value.replace(/\D/g, "");
|
|
324
|
+
let h: number = parseInt(raw || "0", 10);
|
|
325
|
+
if (userPrefers12h) {
|
|
326
|
+
h = clamp(h, 1, 12);
|
|
327
|
+
// map back to 24h preserving period
|
|
328
|
+
const isPM: boolean = tempHours24 >= 12;
|
|
329
|
+
const newH: number =
|
|
330
|
+
h === 12 ? (isPM ? 12 : 0) : isPM ? h + 12 : h;
|
|
331
|
+
setTempHours24(newH);
|
|
332
|
+
} else {
|
|
333
|
+
setTempHours24(clamp(h, 0, 23));
|
|
334
|
+
}
|
|
335
|
+
}}
|
|
336
|
+
/>
|
|
337
|
+
<button
|
|
338
|
+
type="button"
|
|
339
|
+
aria-label="Decrease hours"
|
|
340
|
+
className="p-2 rounded hover:bg-gray-50"
|
|
341
|
+
onClick={(): void => {
|
|
342
|
+
return setTempHours24((h: number): number => {
|
|
343
|
+
return (h + 23) % 24;
|
|
344
|
+
});
|
|
345
|
+
}}
|
|
346
|
+
>
|
|
347
|
+
<Icon icon={IconProp.ChevronDown} className="h-6 w-6" />
|
|
348
|
+
</button>
|
|
349
|
+
</div>
|
|
350
|
+
|
|
351
|
+
<div className="text-3xl font-semibold text-gray-500">:</div>
|
|
352
|
+
|
|
353
|
+
{/* Minutes selector */}
|
|
354
|
+
<div className="flex flex-col items-center">
|
|
355
|
+
<button
|
|
356
|
+
type="button"
|
|
357
|
+
aria-label="Increase minutes"
|
|
358
|
+
className="p-2 rounded hover:bg-gray-50"
|
|
359
|
+
onClick={(): void => {
|
|
360
|
+
let m: number = tempMinutes + 1;
|
|
361
|
+
let h: number = tempHours24;
|
|
362
|
+
if (m >= 60) {
|
|
363
|
+
m = 0;
|
|
364
|
+
h = (h + 1) % 24;
|
|
365
|
+
}
|
|
366
|
+
setTempMinutes(m);
|
|
367
|
+
setTempHours24(h);
|
|
368
|
+
}}
|
|
369
|
+
>
|
|
370
|
+
<Icon icon={IconProp.ChevronUp} className="h-6 w-6" />
|
|
371
|
+
</button>
|
|
372
|
+
<input
|
|
373
|
+
type="text"
|
|
374
|
+
inputMode="numeric"
|
|
375
|
+
pattern="[0-9]*"
|
|
376
|
+
aria-label="Minutes"
|
|
377
|
+
className="w-20 text-center text-3xl font-semibold py-2 rounded border border-gray-200 focus:ring-2 focus:ring-indigo-500"
|
|
378
|
+
value={pad2(tempMinutes)}
|
|
379
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement>): void => {
|
|
380
|
+
const raw: string = e.target.value.replace(/\D/g, "");
|
|
381
|
+
const m: number = clamp(parseInt(raw || "0", 10), 0, 59);
|
|
382
|
+
setTempMinutes(m);
|
|
383
|
+
}}
|
|
384
|
+
/>
|
|
385
|
+
<button
|
|
386
|
+
type="button"
|
|
387
|
+
aria-label="Decrease minutes"
|
|
388
|
+
className="p-2 rounded hover:bg-gray-50"
|
|
389
|
+
onClick={(): void => {
|
|
390
|
+
let m: number = tempMinutes - 1;
|
|
391
|
+
let h: number = tempHours24;
|
|
392
|
+
if (m < 0) {
|
|
393
|
+
m = 59;
|
|
394
|
+
h = (h + 23) % 24;
|
|
395
|
+
}
|
|
396
|
+
setTempMinutes(m);
|
|
397
|
+
setTempHours24(h);
|
|
398
|
+
}}
|
|
399
|
+
>
|
|
400
|
+
<Icon icon={IconProp.ChevronDown} className="h-6 w-6" />
|
|
401
|
+
</button>
|
|
402
|
+
</div>
|
|
403
|
+
|
|
404
|
+
{/* AM/PM */}
|
|
405
|
+
{userPrefers12h && (
|
|
406
|
+
<div className="ml-2 flex flex-col items-stretch gap-2">
|
|
407
|
+
<button
|
|
408
|
+
type="button"
|
|
409
|
+
className={`px-3 py-2 rounded text-sm border ${tempHours24 < 12 ? "bg-indigo-50 text-indigo-700 border-indigo-200" : "bg-white text-gray-700 border-gray-300"}`}
|
|
410
|
+
aria-pressed={tempHours24 < 12}
|
|
411
|
+
onClick={() => {
|
|
412
|
+
if (tempHours24 >= 12) {
|
|
413
|
+
setTempHours24(tempHours24 - 12);
|
|
414
|
+
}
|
|
415
|
+
}}
|
|
416
|
+
>
|
|
417
|
+
AM
|
|
418
|
+
</button>
|
|
419
|
+
<button
|
|
420
|
+
type="button"
|
|
421
|
+
className={`px-3 py-2 rounded text-sm border ${tempHours24 >= 12 ? "bg-indigo-50 text-indigo-700 border-indigo-200" : "bg-white text-gray-700 border-gray-300"}`}
|
|
422
|
+
aria-pressed={tempHours24 >= 12}
|
|
423
|
+
onClick={() => {
|
|
424
|
+
if (tempHours24 < 12) {
|
|
425
|
+
setTempHours24(tempHours24 + 12);
|
|
426
|
+
}
|
|
427
|
+
}}
|
|
428
|
+
>
|
|
429
|
+
PM
|
|
430
|
+
</button>
|
|
431
|
+
</div>
|
|
432
|
+
)}
|
|
433
|
+
</div>
|
|
434
|
+
|
|
435
|
+
{/* Quick minutes */}
|
|
436
|
+
<div className="mt-6">
|
|
437
|
+
<div className="text-sm text-gray-500 mb-2">Quick minutes</div>
|
|
438
|
+
<div className="grid grid-cols-6 gap-2">
|
|
439
|
+
{[0, 5, 10, 15, 30, 45].map((m: number) => {
|
|
440
|
+
return (
|
|
441
|
+
<button
|
|
442
|
+
key={m}
|
|
443
|
+
type="button"
|
|
444
|
+
className={`px-2 py-1 rounded border text-sm ${tempMinutes === m ? "bg-indigo-50 text-indigo-700 border-indigo-200" : "bg-white text-gray-700 border-gray-300"}`}
|
|
445
|
+
onClick={(): void => {
|
|
446
|
+
return setTempMinutes(m);
|
|
447
|
+
}}
|
|
448
|
+
>
|
|
449
|
+
{pad2(m)}
|
|
450
|
+
</button>
|
|
451
|
+
);
|
|
452
|
+
})}
|
|
453
|
+
</div>
|
|
454
|
+
</div>
|
|
455
|
+
<div className="mt-8 text-sm text-gray-500">
|
|
456
|
+
This time is in your {timezoneLabel} timezone.
|
|
457
|
+
</div>
|
|
458
|
+
</div>
|
|
459
|
+
</Modal>
|
|
460
|
+
)}
|
|
461
|
+
|
|
462
|
+
{props.error && (
|
|
463
|
+
<p
|
|
464
|
+
id="timepicker-error"
|
|
465
|
+
data-testid="error-message"
|
|
466
|
+
className="mt-1 text-sm text-red-400"
|
|
467
|
+
aria-live="polite"
|
|
468
|
+
>
|
|
469
|
+
{props.error}
|
|
470
|
+
</p>
|
|
471
|
+
)}
|
|
472
|
+
</>
|
|
473
|
+
);
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
export default TimePicker;
|