playbook_ui 15.7.0 → 15.8.0.pre.rc.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.
- checksums.yaml +4 -4
- data/app/pb_kits/playbook/_playbook.scss +1 -1
- data/app/pb_kits/playbook/pb_fixed_confirmation_toast/fixed_confirmation_toast.rb +9 -7
- data/app/pb_kits/playbook/pb_fixed_confirmation_toast/index.js +3 -8
- data/app/pb_kits/playbook/pb_form/docs/_form_form_with.html.erb +1 -1
- data/app/pb_kits/playbook/pb_form/docs/_form_form_with_validate.html.erb +2 -1
- data/app/pb_kits/playbook/pb_form/docs/_form_with_required_indicator.html.erb +14 -0
- data/app/pb_kits/playbook/pb_form/docs/_form_with_required_indicator.md +3 -0
- data/app/pb_kits/playbook/pb_form/docs/example.yml +1 -0
- data/app/pb_kits/playbook/pb_popover/docs/_popover_append_to.html.erb +2 -2
- data/app/pb_kits/playbook/pb_popover/docs/_popover_append_to.jsx +3 -2
- data/app/pb_kits/playbook/pb_text_input/_text_input.tsx +56 -6
- data/app/pb_kits/playbook/pb_text_input/docs/_text_input_emoji_mask.html.erb +7 -0
- data/app/pb_kits/playbook/pb_text_input/docs/_text_input_emoji_mask.jsx +24 -0
- data/app/pb_kits/playbook/pb_text_input/docs/_text_input_emoji_mask.md +2 -0
- data/app/pb_kits/playbook/pb_text_input/docs/_text_input_required_indicator.html.erb +6 -0
- data/app/pb_kits/playbook/pb_text_input/docs/_text_input_required_indicator.jsx +25 -0
- data/app/pb_kits/playbook/pb_text_input/docs/_text_input_required_indicator.md +3 -0
- data/app/pb_kits/playbook/pb_text_input/docs/example.yml +5 -0
- data/app/pb_kits/playbook/pb_text_input/docs/index.js +2 -0
- data/app/pb_kits/playbook/pb_text_input/index.js +49 -8
- data/app/pb_kits/playbook/pb_text_input/text_input.html.erb +6 -0
- data/app/pb_kits/playbook/pb_text_input/text_input.rb +7 -1
- data/app/pb_kits/playbook/pb_text_input/text_input.test.js +69 -0
- data/app/pb_kits/playbook/pb_textarea/_textarea.tsx +38 -2
- data/app/pb_kits/playbook/pb_textarea/docs/_textarea_emoji_mask.html.erb +5 -0
- data/app/pb_kits/playbook/pb_textarea/docs/_textarea_emoji_mask.jsx +24 -0
- data/app/pb_kits/playbook/pb_textarea/docs/_textarea_emoji_mask.md +1 -0
- data/app/pb_kits/playbook/pb_textarea/docs/example.yml +2 -0
- data/app/pb_kits/playbook/pb_textarea/docs/index.js +1 -0
- data/app/pb_kits/playbook/pb_textarea/index.ts +62 -5
- data/app/pb_kits/playbook/pb_textarea/textarea.html.erb +1 -0
- data/app/pb_kits/playbook/pb_textarea/textarea.rb +8 -0
- data/app/pb_kits/playbook/pb_textarea/textarea.test.js +57 -2
- data/app/pb_kits/playbook/pb_time_picker/_time_picker.scss +296 -0
- data/app/pb_kits/playbook/pb_time_picker/_time_picker.tsx +822 -0
- data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_24_hour.html.erb +2 -0
- data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_24_hour.jsx +16 -0
- data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_24_hour.md +1 -0
- data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_default.html.erb +1 -0
- data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_default.jsx +13 -0
- data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_default.md +1 -0
- data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_default_time.html.erb +4 -0
- data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_default_time.jsx +29 -0
- data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_default_time.md +1 -0
- data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_disabled.html.erb +13 -0
- data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_disabled.jsx +23 -0
- data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_error.html.erb +5 -0
- data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_error.jsx +15 -0
- data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_input_options.html.erb +14 -0
- data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_label.html.erb +2 -0
- data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_label.jsx +15 -0
- data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_min_max_time.html.erb +42 -0
- data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_min_max_time.jsx +52 -0
- data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_min_max_time.md +1 -0
- data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_on_handler.jsx +45 -0
- data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_on_handler.md +1 -0
- data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_timezone.html.erb +3 -0
- data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_timezone.jsx +21 -0
- data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_timezone.md +1 -0
- data/app/pb_kits/playbook/pb_time_picker/docs/example.yml +24 -0
- data/app/pb_kits/playbook/pb_time_picker/docs/index.js +9 -0
- data/app/pb_kits/playbook/pb_time_picker/index.ts +40 -0
- data/app/pb_kits/playbook/pb_time_picker/time_picker.html.erb +1 -0
- data/app/pb_kits/playbook/pb_time_picker/time_picker.rb +80 -0
- data/app/pb_kits/playbook/pb_time_picker/time_picker.test.jsx +114 -0
- data/app/pb_kits/playbook/pb_time_picker/time_picker_helper.ts +662 -0
- data/app/pb_kits/playbook/utilities/emojiMask.ts +42 -0
- data/app/pb_kits/playbook/utilities/globalProps.ts +1 -0
- data/dist/chunks/_typeahead-CSCNg6cp.js +6 -0
- data/dist/chunks/lib-DxCgrqqG.js +29 -0
- data/dist/chunks/vendor.js +3 -3
- data/dist/menu.yml +7 -0
- data/dist/playbook-rails-react-bindings.js +1 -1
- data/dist/playbook-rails.js +1 -1
- data/dist/playbook.css +1 -1
- data/lib/playbook/forms/builder/form_field_builder.rb +15 -2
- data/lib/playbook/forms/builder/time_picker_field.rb +24 -0
- data/lib/playbook/forms/builder.rb +1 -0
- data/lib/playbook/version.rb +2 -2
- metadata +50 -4
- data/dist/chunks/_typeahead-X3EqK1nR.js +0 -6
- data/dist/chunks/lib-BHYZzndy.js +0 -29
|
@@ -0,0 +1,822 @@
|
|
|
1
|
+
import React, { useState, useEffect, useRef, useMemo } from 'react'
|
|
2
|
+
import classnames from 'classnames'
|
|
3
|
+
import { buildAriaProps, buildCss, buildDataProps, buildHtmlProps } from '../utilities/props'
|
|
4
|
+
import { globalProps, GlobalProps } from '../utilities/globalProps'
|
|
5
|
+
import Caption from '../pb_caption/_caption'
|
|
6
|
+
import SelectableCard from '../pb_selectable_card/_selectable_card'
|
|
7
|
+
import TextInput from '../pb_text_input/_text_input'
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
parseTime,
|
|
11
|
+
parseTimeToMinutes,
|
|
12
|
+
isTimeInRange as isTimeInRangeHelper,
|
|
13
|
+
isHourDisabled as isHourDisabledHelper,
|
|
14
|
+
isAnyAMTimeValid as isAnyAMTimeValidHelper,
|
|
15
|
+
isAnyPMTimeValid as isAnyPMTimeValidHelper,
|
|
16
|
+
getDisplayTime,
|
|
17
|
+
get24HourTime,
|
|
18
|
+
getTimeRangeErrorMessage,
|
|
19
|
+
getTimezoneText,
|
|
20
|
+
getHourConstraints,
|
|
21
|
+
processHourInput,
|
|
22
|
+
normalizeHourOnBlur,
|
|
23
|
+
processMinuteInput,
|
|
24
|
+
normalizeMinuteOnBlur,
|
|
25
|
+
convertTo24HourFormat,
|
|
26
|
+
convertTo12HourFormat,
|
|
27
|
+
generateHourOptions,
|
|
28
|
+
generateMinuteOptions,
|
|
29
|
+
getValidInitialMeridiem as getValidInitialMeridiemHelper,
|
|
30
|
+
TimeFormat,
|
|
31
|
+
ParsedTime,
|
|
32
|
+
} from './time_picker_helper'
|
|
33
|
+
|
|
34
|
+
type TimePickerProps = {
|
|
35
|
+
aria?: { [key: string]: string },
|
|
36
|
+
className?: string,
|
|
37
|
+
data?: { [key: string]: string },
|
|
38
|
+
defaultTime?: string,
|
|
39
|
+
disabled?: boolean,
|
|
40
|
+
error?: string,
|
|
41
|
+
htmlOptions?: { [key: string]: string | number | boolean | (() => void) | ((arg?: Event) => void) },
|
|
42
|
+
id?: string,
|
|
43
|
+
inputOptions?: { [key: string]: string | number | boolean | (() => void) | ((arg?: Event) => void) },
|
|
44
|
+
label?: string,
|
|
45
|
+
maxTime?: string,
|
|
46
|
+
minTime?: string,
|
|
47
|
+
name?: string,
|
|
48
|
+
onChange?: (time: string) => void,
|
|
49
|
+
onClose?: (time: string) => void,
|
|
50
|
+
required?: boolean,
|
|
51
|
+
showTimezone?: boolean,
|
|
52
|
+
timeFormat?: TimeFormat,
|
|
53
|
+
validationMessage?: string,
|
|
54
|
+
value?: string,
|
|
55
|
+
} & GlobalProps
|
|
56
|
+
|
|
57
|
+
const TimePicker = (props: TimePickerProps): JSX.Element => {
|
|
58
|
+
const {
|
|
59
|
+
aria = {},
|
|
60
|
+
className,
|
|
61
|
+
data = {},
|
|
62
|
+
defaultTime,
|
|
63
|
+
disabled = false,
|
|
64
|
+
error,
|
|
65
|
+
htmlOptions = {},
|
|
66
|
+
id,
|
|
67
|
+
inputOptions = {},
|
|
68
|
+
label = 'Time Picker',
|
|
69
|
+
maxTime,
|
|
70
|
+
minTime,
|
|
71
|
+
name,
|
|
72
|
+
onChange,
|
|
73
|
+
onClose,
|
|
74
|
+
required = false,
|
|
75
|
+
showTimezone = false,
|
|
76
|
+
timeFormat = 'AMPM',
|
|
77
|
+
validationMessage,
|
|
78
|
+
value,
|
|
79
|
+
} = props
|
|
80
|
+
|
|
81
|
+
// Form Validation Tracking for React Rendered Rails Kit
|
|
82
|
+
const [formSubmitted, setFormSubmitted] = useState(false)
|
|
83
|
+
|
|
84
|
+
const uniqueId = useMemo(() => {
|
|
85
|
+
return id || `time-picker-${Math.random().toString(36).substr(2, 9)}`
|
|
86
|
+
}, [id])
|
|
87
|
+
|
|
88
|
+
const fieldName = name || `${uniqueId}-time`
|
|
89
|
+
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
const handleInvalid = (event: Event) => {
|
|
92
|
+
const target = event.target as HTMLInputElement
|
|
93
|
+
const timePickerContainer = target.closest('[data-pb-react-component="TimePicker"]')
|
|
94
|
+
|
|
95
|
+
if (timePickerContainer) {
|
|
96
|
+
const invalidInputName = target.name || target.getAttribute('name')
|
|
97
|
+
if (invalidInputName === fieldName) {
|
|
98
|
+
setFormSubmitted(true)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
document.addEventListener('invalid', handleInvalid, true)
|
|
103
|
+
|
|
104
|
+
return () => {
|
|
105
|
+
document.removeEventListener('invalid', handleInvalid, true)
|
|
106
|
+
}
|
|
107
|
+
}, [fieldName])
|
|
108
|
+
|
|
109
|
+
// Min/Max Time Range Validation
|
|
110
|
+
const minTimeMinutes = parseTimeToMinutes(minTime)
|
|
111
|
+
const maxTimeMinutes = parseTimeToMinutes(maxTime)
|
|
112
|
+
|
|
113
|
+
const isTimeInRange = (h: number, m: number, mer?: 'AM' | 'PM'): boolean => {
|
|
114
|
+
return isTimeInRangeHelper(h, m, mer, timeFormat, minTimeMinutes, maxTimeMinutes)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const isHourDisabled = (h: number, mer?: 'AM' | 'PM'): boolean => {
|
|
118
|
+
return isHourDisabledHelper(h, mer, timeFormat, minTimeMinutes, maxTimeMinutes)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const isCurrentTimeValid = (h: number, m: number, mer: 'AM' | 'PM'): boolean => {
|
|
122
|
+
return isTimeInRange(h, m, mer)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const isAnyAMTimeValid = (): boolean => {
|
|
126
|
+
return isAnyAMTimeValidHelper(minTimeMinutes, maxTimeMinutes)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const isAnyPMTimeValid = (): boolean => {
|
|
130
|
+
return isAnyPMTimeValidHelper(minTimeMinutes, maxTimeMinutes)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Wrapper for helper function with component's min/max context
|
|
134
|
+
const getValidInitialMeridiem = (parsedMeridiem: 'AM' | 'PM'): 'AM' | 'PM' => {
|
|
135
|
+
return getValidInitialMeridiemHelper(parsedMeridiem, minTimeMinutes, maxTimeMinutes)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const hasInitialValue = !!(value || defaultTime)
|
|
139
|
+
const initialTime = parseTime(value || defaultTime, timeFormat)
|
|
140
|
+
const validInitialMeridiem = getValidInitialMeridiem(initialTime.meridiem)
|
|
141
|
+
const [hour, setHour] = useState(initialTime.hour)
|
|
142
|
+
const [minute, setMinute] = useState(initialTime.minute)
|
|
143
|
+
const [meridiem, setMeridiem] = useState<'AM' | 'PM'>(validInitialMeridiem)
|
|
144
|
+
const [hasSelectedTime, setHasSelectedTime] = useState(hasInitialValue)
|
|
145
|
+
const [hourInputValue, setHourInputValue] = useState<string>(initialTime.hour.toString())
|
|
146
|
+
const [minuteInputValue, setMinuteInputValue] = useState<string>(initialTime.minute.toString().padStart(2, '0'))
|
|
147
|
+
|
|
148
|
+
// Update hour input value format based on timeFormat changes
|
|
149
|
+
const prevTimeFormatRef = useRef(timeFormat)
|
|
150
|
+
const isInitialMountRef = useRef(true)
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
if (isInitialMountRef.current) {
|
|
153
|
+
isInitialMountRef.current = false
|
|
154
|
+
prevTimeFormatRef.current = timeFormat
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (prevTimeFormatRef.current === timeFormat) return
|
|
159
|
+
prevTimeFormatRef.current = timeFormat
|
|
160
|
+
|
|
161
|
+
if (timeFormat === '24hour') {
|
|
162
|
+
const result = convertTo24HourFormat(hour, meridiem)
|
|
163
|
+
setHour(result.hour)
|
|
164
|
+
setHourInputValue(result.hour.toString())
|
|
165
|
+
if (result.meridiem !== meridiem) {
|
|
166
|
+
setMeridiem(result.meridiem)
|
|
167
|
+
}
|
|
168
|
+
} else {
|
|
169
|
+
const result = convertTo12HourFormat(hour, meridiem)
|
|
170
|
+
setHour(result.hour)
|
|
171
|
+
setHourInputValue(result.hour.toString())
|
|
172
|
+
if (result.meridiem !== meridiem) {
|
|
173
|
+
setMeridiem(result.meridiem)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
177
|
+
}, [timeFormat])
|
|
178
|
+
|
|
179
|
+
const hourInputRef = useRef<HTMLInputElement>(null)
|
|
180
|
+
const minuteInputRef = useRef<HTMLInputElement>(null)
|
|
181
|
+
const amInputRef = useRef<HTMLInputElement | null>(null)
|
|
182
|
+
const pmInputRef = useRef<HTMLInputElement | null>(null)
|
|
183
|
+
const timePickerWrapperRef = useRef<HTMLDivElement>(null)
|
|
184
|
+
const hourDropdownRef = useRef<HTMLDivElement>(null)
|
|
185
|
+
const minuteDropdownRef = useRef<HTMLDivElement>(null)
|
|
186
|
+
const [showDropdown, setShowDropdown] = useState(false)
|
|
187
|
+
const [showHourDropdown, setShowHourDropdown] = useState(false)
|
|
188
|
+
const [showMinuteDropdown, setShowMinuteDropdown] = useState(false)
|
|
189
|
+
|
|
190
|
+
// Input dropdown scrolling
|
|
191
|
+
const scrollDropdownToSelected = (dropdownRef: React.RefObject<HTMLDivElement>) => {
|
|
192
|
+
if (dropdownRef.current) {
|
|
193
|
+
const selectedOption = dropdownRef.current.querySelector('.selected') as HTMLElement
|
|
194
|
+
if (selectedOption) {
|
|
195
|
+
const dropdown = dropdownRef.current
|
|
196
|
+
const dropdownHeight = dropdown.clientHeight
|
|
197
|
+
const optionTop = selectedOption.offsetTop
|
|
198
|
+
const optionHeight = selectedOption.clientHeight
|
|
199
|
+
// Center the selected option in the dropdown
|
|
200
|
+
dropdown.scrollTop = optionTop - (dropdownHeight / 2) + (optionHeight / 2)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
useEffect(() => {
|
|
206
|
+
if (showHourDropdown) {
|
|
207
|
+
scrollDropdownToSelected(hourDropdownRef)
|
|
208
|
+
}
|
|
209
|
+
}, [showHourDropdown])
|
|
210
|
+
|
|
211
|
+
useEffect(() => {
|
|
212
|
+
if (showMinuteDropdown) {
|
|
213
|
+
scrollDropdownToSelected(minuteDropdownRef)
|
|
214
|
+
}
|
|
215
|
+
}, [showMinuteDropdown])
|
|
216
|
+
|
|
217
|
+
useEffect(() => {
|
|
218
|
+
if (showHourDropdown) {
|
|
219
|
+
setTimeout(() => scrollDropdownToSelected(hourDropdownRef), 0)
|
|
220
|
+
}
|
|
221
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
222
|
+
}, [hour])
|
|
223
|
+
|
|
224
|
+
useEffect(() => {
|
|
225
|
+
if (showMinuteDropdown) {
|
|
226
|
+
setTimeout(() => scrollDropdownToSelected(minuteDropdownRef), 0)
|
|
227
|
+
}
|
|
228
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
229
|
+
}, [minute])
|
|
230
|
+
|
|
231
|
+
const [displayValue, setDisplayValue] = useState(
|
|
232
|
+
hasInitialValue ? getDisplayTime(hour, minute, meridiem, timeFormat) : ''
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
// Clearing Input and Validation Code
|
|
236
|
+
// Track the last valid time for reverting invalid selections
|
|
237
|
+
const [lastValidTime, setLastValidTime] = useState<ParsedTime | null>(
|
|
238
|
+
hasInitialValue && isTimeInRange(initialTime.hour, initialTime.minute, initialTime.meridiem)
|
|
239
|
+
? initialTime
|
|
240
|
+
: null
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
// Clear the time picker value completely
|
|
244
|
+
const clearTimePicker = () => {
|
|
245
|
+
setHasSelectedTime(false)
|
|
246
|
+
setLastValidTime(null)
|
|
247
|
+
setDisplayValue('')
|
|
248
|
+
const defaultState = parseTime(undefined, timeFormat)
|
|
249
|
+
const validMeridiem = getValidInitialMeridiem(defaultState.meridiem)
|
|
250
|
+
setHour(defaultState.hour)
|
|
251
|
+
setMinute(defaultState.minute)
|
|
252
|
+
setMeridiem(validMeridiem)
|
|
253
|
+
setHourInputValue(defaultState.hour.toString())
|
|
254
|
+
setMinuteInputValue(defaultState.minute.toString().padStart(2, '0'))
|
|
255
|
+
if (onChange) {
|
|
256
|
+
onChange('')
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Close dropdown and handle validation
|
|
261
|
+
const closeDropdown = (skipValidation = false) => {
|
|
262
|
+
setShowHourDropdown(false)
|
|
263
|
+
setShowMinuteDropdown(false)
|
|
264
|
+
|
|
265
|
+
// If user hasn't selected anything, just close the dropdown
|
|
266
|
+
if (!hasSelectedTime) {
|
|
267
|
+
setShowDropdown(false)
|
|
268
|
+
return
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const currentTimeValid = skipValidation || isCurrentTimeValid(hour, minute, meridiem)
|
|
272
|
+
|
|
273
|
+
if (currentTimeValid) {
|
|
274
|
+
// Valid time - save it
|
|
275
|
+
const timeString = get24HourTime(hour, minute, meridiem, timeFormat)
|
|
276
|
+
setDisplayValue(getDisplayTime(hour, minute, meridiem, timeFormat))
|
|
277
|
+
setLastValidTime({ hour, minute, meridiem })
|
|
278
|
+
setShowDropdown(false)
|
|
279
|
+
if (onClose) {
|
|
280
|
+
onClose(timeString)
|
|
281
|
+
}
|
|
282
|
+
} else {
|
|
283
|
+
// Invalid time - revert to last valid time or reset to defaults
|
|
284
|
+
if (lastValidTime) {
|
|
285
|
+
setHour(lastValidTime.hour)
|
|
286
|
+
setMinute(lastValidTime.minute)
|
|
287
|
+
setMeridiem(lastValidTime.meridiem)
|
|
288
|
+
setHourInputValue(lastValidTime.hour.toString())
|
|
289
|
+
setMinuteInputValue(lastValidTime.minute.toString().padStart(2, '0'))
|
|
290
|
+
setDisplayValue(getDisplayTime(lastValidTime.hour, lastValidTime.minute, lastValidTime.meridiem, timeFormat))
|
|
291
|
+
} else {
|
|
292
|
+
// No valid time ever selected - reset to default state
|
|
293
|
+
const defaultState = parseTime(undefined, timeFormat)
|
|
294
|
+
const validMeridiem = getValidInitialMeridiem(defaultState.meridiem)
|
|
295
|
+
setHour(defaultState.hour)
|
|
296
|
+
setMinute(defaultState.minute)
|
|
297
|
+
setMeridiem(validMeridiem)
|
|
298
|
+
setHourInputValue(defaultState.hour.toString())
|
|
299
|
+
setMinuteInputValue(defaultState.minute.toString().padStart(2, '0'))
|
|
300
|
+
setDisplayValue('')
|
|
301
|
+
setHasSelectedTime(false)
|
|
302
|
+
}
|
|
303
|
+
setShowDropdown(false)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Handle controlled component updates
|
|
308
|
+
useEffect(() => {
|
|
309
|
+
if (value !== undefined) {
|
|
310
|
+
const parsed = parseTime(value, timeFormat)
|
|
311
|
+
setHour(parsed.hour)
|
|
312
|
+
setMinute(parsed.minute)
|
|
313
|
+
setMeridiem(parsed.meridiem)
|
|
314
|
+
setHourInputValue(parsed.hour.toString())
|
|
315
|
+
setMinuteInputValue(parsed.minute.toString().padStart(2, '0'))
|
|
316
|
+
setDisplayValue(getDisplayTime(parsed.hour, parsed.minute, parsed.meridiem, timeFormat))
|
|
317
|
+
setHasSelectedTime(true)
|
|
318
|
+
}
|
|
319
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
320
|
+
}, [value])
|
|
321
|
+
|
|
322
|
+
// Re-parse when timeFormat changes if we have a defaultTime
|
|
323
|
+
const prevTimeFormatRef3 = useRef(timeFormat)
|
|
324
|
+
const isInitialMountRef3 = useRef(true)
|
|
325
|
+
useEffect(() => {
|
|
326
|
+
if (isInitialMountRef3.current) {
|
|
327
|
+
isInitialMountRef3.current = false
|
|
328
|
+
prevTimeFormatRef3.current = timeFormat
|
|
329
|
+
return
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (prevTimeFormatRef3.current === timeFormat) return
|
|
333
|
+
prevTimeFormatRef3.current = timeFormat
|
|
334
|
+
|
|
335
|
+
if (defaultTime && !value) {
|
|
336
|
+
const parsed = parseTime(defaultTime, timeFormat)
|
|
337
|
+
setHour(parsed.hour)
|
|
338
|
+
setMinute(parsed.minute)
|
|
339
|
+
setMeridiem(parsed.meridiem)
|
|
340
|
+
setHourInputValue(parsed.hour.toString())
|
|
341
|
+
setMinuteInputValue(parsed.minute.toString().padStart(2, '0'))
|
|
342
|
+
setDisplayValue(getDisplayTime(parsed.hour, parsed.minute, parsed.meridiem, timeFormat))
|
|
343
|
+
}
|
|
344
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
345
|
+
}, [timeFormat])
|
|
346
|
+
|
|
347
|
+
useEffect(() => {
|
|
348
|
+
if (hasSelectedTime) {
|
|
349
|
+
setDisplayValue(getDisplayTime(hour, minute, meridiem, timeFormat))
|
|
350
|
+
}
|
|
351
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
352
|
+
}, [hour, minute, meridiem, hasSelectedTime, timeFormat])
|
|
353
|
+
|
|
354
|
+
const handleHourChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
355
|
+
const rawValue = e.target.value
|
|
356
|
+
const result = processHourInput(rawValue, timeFormat)
|
|
357
|
+
|
|
358
|
+
if (!result.isValid) return
|
|
359
|
+
|
|
360
|
+
setHourInputValue(result.value)
|
|
361
|
+
|
|
362
|
+
if (result.hour !== null) {
|
|
363
|
+
setHour(result.hour)
|
|
364
|
+
setHasSelectedTime(true)
|
|
365
|
+
const timeString = get24HourTime(result.hour, minute, meridiem, timeFormat)
|
|
366
|
+
if (onChange) {
|
|
367
|
+
onChange(timeString)
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const handleHourBlur = () => {
|
|
373
|
+
const result = normalizeHourOnBlur(hourInputValue, hour, timeFormat)
|
|
374
|
+
setHour(result.hour)
|
|
375
|
+
setHourInputValue(result.displayValue)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const handleMinuteChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
379
|
+
const rawValue = e.target.value
|
|
380
|
+
const result = processMinuteInput(rawValue)
|
|
381
|
+
|
|
382
|
+
if (!result.isValid) return
|
|
383
|
+
|
|
384
|
+
setMinuteInputValue(result.value)
|
|
385
|
+
|
|
386
|
+
if (result.minute !== null) {
|
|
387
|
+
setMinute(result.minute)
|
|
388
|
+
setHasSelectedTime(true)
|
|
389
|
+
const timeString = get24HourTime(hour, result.minute, meridiem, timeFormat)
|
|
390
|
+
if (onChange) {
|
|
391
|
+
onChange(timeString)
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const handleMinuteBlur = () => {
|
|
397
|
+
const result = normalizeMinuteOnBlur(minuteInputValue, minute)
|
|
398
|
+
setMinute(result.minute)
|
|
399
|
+
setMinuteInputValue(result.displayValue)
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const handleHourOptionClick = (h: number) => {
|
|
403
|
+
setHour(h)
|
|
404
|
+
setHourInputValue(h.toString())
|
|
405
|
+
setHasSelectedTime(true)
|
|
406
|
+
setShowHourDropdown(false)
|
|
407
|
+
const timeString = get24HourTime(h, minute, meridiem, timeFormat)
|
|
408
|
+
if (onChange) {
|
|
409
|
+
onChange(timeString)
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const handleMinuteOptionClick = (m: number) => {
|
|
414
|
+
setMinute(m)
|
|
415
|
+
setMinuteInputValue(m.toString().padStart(2, '0'))
|
|
416
|
+
setHasSelectedTime(true)
|
|
417
|
+
setShowMinuteDropdown(false)
|
|
418
|
+
const timeString = get24HourTime(hour, m, meridiem, timeFormat)
|
|
419
|
+
if (onChange) {
|
|
420
|
+
onChange(timeString)
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const handleMeridiemChange = (mer: 'AM' | 'PM') => {
|
|
425
|
+
setMeridiem(mer)
|
|
426
|
+
setHasSelectedTime(true)
|
|
427
|
+
const timeString = get24HourTime(hour, minute, mer, timeFormat)
|
|
428
|
+
if (onChange) {
|
|
429
|
+
onChange(timeString)
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const handleInputClick = () => {
|
|
434
|
+
setShowDropdown(true)
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const handleInputFocus = () => {
|
|
438
|
+
setShowDropdown(true)
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
442
|
+
if (e.key === 'Enter') {
|
|
443
|
+
e.preventDefault()
|
|
444
|
+
closeDropdown()
|
|
445
|
+
} else if (e.key === 'Escape') {
|
|
446
|
+
e.preventDefault()
|
|
447
|
+
closeDropdown()
|
|
448
|
+
} else if (e.key === 'Tab' && !e.shiftKey && showDropdown) {
|
|
449
|
+
e.preventDefault()
|
|
450
|
+
hourInputRef.current?.focus()
|
|
451
|
+
} else if (e.key === 'Tab' && e.shiftKey) {
|
|
452
|
+
// Allow shift+tab to go to previous element
|
|
453
|
+
return
|
|
454
|
+
} else if (e.key === 'Backspace' || e.key === 'Delete') {
|
|
455
|
+
// Allow clearing the input
|
|
456
|
+
e.preventDefault()
|
|
457
|
+
clearTimePicker()
|
|
458
|
+
} else if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
|
459
|
+
// Open dropdown on arrow keys
|
|
460
|
+
e.preventDefault()
|
|
461
|
+
if (!showDropdown) {
|
|
462
|
+
setShowDropdown(true)
|
|
463
|
+
}
|
|
464
|
+
} else {
|
|
465
|
+
// Prevent typing in main input - alternative to readonly that allows for validation to occur
|
|
466
|
+
e.preventDefault()
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const handleHourKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
471
|
+
if (e.key === 'Tab') {
|
|
472
|
+
if (!e.shiftKey) {
|
|
473
|
+
e.preventDefault()
|
|
474
|
+
setShowHourDropdown(false)
|
|
475
|
+
minuteInputRef.current?.focus()
|
|
476
|
+
}
|
|
477
|
+
} else if (e.key === 'Enter') {
|
|
478
|
+
e.preventDefault()
|
|
479
|
+
setShowHourDropdown(false)
|
|
480
|
+
closeDropdown()
|
|
481
|
+
} else if (e.key === 'Escape') {
|
|
482
|
+
e.preventDefault()
|
|
483
|
+
setShowHourDropdown(false)
|
|
484
|
+
closeDropdown()
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const handleMinuteKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
489
|
+
if (e.key === 'Tab' && e.shiftKey) {
|
|
490
|
+
e.preventDefault()
|
|
491
|
+
setShowMinuteDropdown(false)
|
|
492
|
+
hourInputRef.current?.focus()
|
|
493
|
+
} else if (e.key === 'Tab') {
|
|
494
|
+
if (!e.shiftKey) {
|
|
495
|
+
e.preventDefault()
|
|
496
|
+
setShowMinuteDropdown(false)
|
|
497
|
+
if (timeFormat === '24hour') {
|
|
498
|
+
closeDropdown()
|
|
499
|
+
} else {
|
|
500
|
+
// Tab to the currently selected meridiem
|
|
501
|
+
if (meridiem === 'AM') {
|
|
502
|
+
amInputRef.current?.focus()
|
|
503
|
+
} else {
|
|
504
|
+
pmInputRef.current?.focus()
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
} else if (e.key === 'Enter') {
|
|
509
|
+
e.preventDefault()
|
|
510
|
+
setShowMinuteDropdown(false)
|
|
511
|
+
closeDropdown()
|
|
512
|
+
} else if (e.key === 'Escape') {
|
|
513
|
+
e.preventDefault()
|
|
514
|
+
setShowMinuteDropdown(false)
|
|
515
|
+
closeDropdown()
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Shared handler for AM/PM - arrow keys toggle between them
|
|
520
|
+
const handleMeridiemKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
521
|
+
if (e.key === 'Tab' && e.shiftKey) {
|
|
522
|
+
e.preventDefault()
|
|
523
|
+
minuteInputRef.current?.focus()
|
|
524
|
+
} else if (e.key === 'Tab' && !e.shiftKey) {
|
|
525
|
+
// Tab out of the component - let it close
|
|
526
|
+
e.preventDefault()
|
|
527
|
+
closeDropdown()
|
|
528
|
+
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
|
|
529
|
+
e.preventDefault()
|
|
530
|
+
// Toggle to AM and focus it
|
|
531
|
+
if (meridiem !== 'AM' && isAnyAMTimeValid()) {
|
|
532
|
+
handleMeridiemChange('AM')
|
|
533
|
+
amInputRef.current?.focus()
|
|
534
|
+
}
|
|
535
|
+
} else if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
|
|
536
|
+
e.preventDefault()
|
|
537
|
+
// Toggle to PM and focus it
|
|
538
|
+
if (meridiem !== 'PM' && isAnyPMTimeValid()) {
|
|
539
|
+
handleMeridiemChange('PM')
|
|
540
|
+
pmInputRef.current?.focus()
|
|
541
|
+
}
|
|
542
|
+
} else if (e.key === 'Enter' || e.key === ' ') {
|
|
543
|
+
// Enter or Space confirms selection and closes
|
|
544
|
+
e.preventDefault()
|
|
545
|
+
closeDropdown()
|
|
546
|
+
} else if (e.key === 'Escape') {
|
|
547
|
+
e.preventDefault()
|
|
548
|
+
closeDropdown()
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Set up keyboard handlers for AM/PM inputs
|
|
553
|
+
useEffect(() => {
|
|
554
|
+
if (showDropdown && timeFormat === 'AMPM') {
|
|
555
|
+
const amInput = document.getElementById(`${uniqueId}-am`) as HTMLInputElement
|
|
556
|
+
const pmInput = document.getElementById(`${uniqueId}-pm`) as HTMLInputElement
|
|
557
|
+
|
|
558
|
+
const handleKeyDownEvent = (e: KeyboardEvent) => {
|
|
559
|
+
const reactEvent = e as unknown as React.KeyboardEvent<HTMLInputElement>
|
|
560
|
+
handleMeridiemKeyDown(reactEvent)
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (amInput) {
|
|
564
|
+
(amInputRef as React.MutableRefObject<HTMLInputElement | null>).current = amInput
|
|
565
|
+
amInput.addEventListener('keydown', handleKeyDownEvent)
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if (pmInput) {
|
|
569
|
+
(pmInputRef as React.MutableRefObject<HTMLInputElement | null>).current = pmInput
|
|
570
|
+
pmInput.addEventListener('keydown', handleKeyDownEvent)
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
return () => {
|
|
574
|
+
if (amInput) amInput.removeEventListener('keydown', handleKeyDownEvent)
|
|
575
|
+
if (pmInput) pmInput.removeEventListener('keydown', handleKeyDownEvent)
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
579
|
+
}, [showDropdown, timeFormat, uniqueId, meridiem])
|
|
580
|
+
|
|
581
|
+
useEffect(() => {
|
|
582
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
583
|
+
if (timePickerWrapperRef.current && !timePickerWrapperRef.current.contains(event.target as Node)) {
|
|
584
|
+
closeDropdown()
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if (showDropdown) {
|
|
589
|
+
document.addEventListener('mousedown', handleClickOutside)
|
|
590
|
+
return () => {
|
|
591
|
+
document.removeEventListener('mousedown', handleClickOutside)
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
595
|
+
}, [showDropdown, hour, minute, meridiem, onClose, lastValidTime, minTime, maxTime])
|
|
596
|
+
|
|
597
|
+
const ariaProps = buildAriaProps(aria)
|
|
598
|
+
const dataProps = buildDataProps(data)
|
|
599
|
+
const htmlProps = buildHtmlProps(htmlOptions)
|
|
600
|
+
const inputHtmlProps = buildHtmlProps(inputOptions)
|
|
601
|
+
|
|
602
|
+
const shouldShowValidationError = required && formSubmitted && !hasSelectedTime
|
|
603
|
+
const errorDisplay = error || (shouldShowValidationError ? (validationMessage || "Please fill out this field.") : "")
|
|
604
|
+
|
|
605
|
+
const classes = classnames(
|
|
606
|
+
buildCss('pb_time_picker'),
|
|
607
|
+
globalProps(props),
|
|
608
|
+
errorDisplay ? 'error' : null,
|
|
609
|
+
disabled ? 'disabled' : null,
|
|
610
|
+
className
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
// Get hour constraints for the input
|
|
614
|
+
const { maxHour, minHour } = getHourConstraints(timeFormat)
|
|
615
|
+
|
|
616
|
+
return (
|
|
617
|
+
<div
|
|
618
|
+
{...ariaProps}
|
|
619
|
+
{...dataProps}
|
|
620
|
+
{...htmlProps}
|
|
621
|
+
className={classes}
|
|
622
|
+
id={uniqueId}
|
|
623
|
+
ref={timePickerWrapperRef}
|
|
624
|
+
style={{ position: 'relative' }}
|
|
625
|
+
>
|
|
626
|
+
{label && (
|
|
627
|
+
<label htmlFor={`${uniqueId}-input`}>
|
|
628
|
+
<Caption
|
|
629
|
+
className="pb_time_picker_kit_label"
|
|
630
|
+
marginBottom="xs"
|
|
631
|
+
size="md"
|
|
632
|
+
text={label}
|
|
633
|
+
/>
|
|
634
|
+
</label>
|
|
635
|
+
)}
|
|
636
|
+
<div className="time_picker_wrapper">
|
|
637
|
+
<TextInput
|
|
638
|
+
addOn={{ icon: 'clock', alignment: 'right', border: true }}
|
|
639
|
+
cursor="pointer"
|
|
640
|
+
disabled={disabled}
|
|
641
|
+
error={errorDisplay}
|
|
642
|
+
id={`${uniqueId}-input`}
|
|
643
|
+
label=""
|
|
644
|
+
name={fieldName}
|
|
645
|
+
onClick={disabled ? undefined : handleInputClick}
|
|
646
|
+
onFocus={disabled ? undefined : handleInputFocus}
|
|
647
|
+
placeholder="Select Time"
|
|
648
|
+
required={required}
|
|
649
|
+
type="text"
|
|
650
|
+
value={displayValue}
|
|
651
|
+
>
|
|
652
|
+
<input
|
|
653
|
+
autoComplete="off"
|
|
654
|
+
disabled={disabled}
|
|
655
|
+
id={`${uniqueId}-input`}
|
|
656
|
+
name={fieldName}
|
|
657
|
+
onChange={() => { /* onChange handled via dropdown selection */ }}
|
|
658
|
+
onClick={disabled ? undefined : handleInputClick}
|
|
659
|
+
onFocus={disabled ? undefined : handleInputFocus}
|
|
660
|
+
onKeyDown={disabled ? undefined : handleInputKeyDown}
|
|
661
|
+
placeholder="Select Time"
|
|
662
|
+
required={required}
|
|
663
|
+
style={{ caretColor: 'transparent' }}
|
|
664
|
+
type="text"
|
|
665
|
+
value={displayValue}
|
|
666
|
+
{...inputHtmlProps}
|
|
667
|
+
/>
|
|
668
|
+
</TextInput>
|
|
669
|
+
|
|
670
|
+
{showDropdown && !disabled && (
|
|
671
|
+
<div className={`pb_time_picker_container ${timeFormat === '24hour' ? 'pb_time_picker_container_24hour' : ''}`}>
|
|
672
|
+
<div className="pb_time_selection">
|
|
673
|
+
<div className="time_input_wrapper">
|
|
674
|
+
<label htmlFor={`${uniqueId}-hour`}>
|
|
675
|
+
<Caption
|
|
676
|
+
className="time_input_label"
|
|
677
|
+
size="sm"
|
|
678
|
+
text="Hour"
|
|
679
|
+
/>
|
|
680
|
+
</label>
|
|
681
|
+
<input
|
|
682
|
+
className={`time_input time-hour ${hasSelectedTime && !isCurrentTimeValid(hour, minute, meridiem) ? 'invalid' : ''}`}
|
|
683
|
+
id={`${uniqueId}-hour`}
|
|
684
|
+
inputMode="numeric"
|
|
685
|
+
max={maxHour}
|
|
686
|
+
maxLength={2}
|
|
687
|
+
min={minHour}
|
|
688
|
+
name={`${uniqueId}-hour`}
|
|
689
|
+
onBlur={handleHourBlur}
|
|
690
|
+
onChange={handleHourChange}
|
|
691
|
+
onClick={() => { setShowHourDropdown(!showHourDropdown); setShowMinuteDropdown(false) }}
|
|
692
|
+
onKeyDown={handleHourKeyDown}
|
|
693
|
+
pattern="[0-9]*"
|
|
694
|
+
ref={hourInputRef}
|
|
695
|
+
step={1}
|
|
696
|
+
tabIndex={0}
|
|
697
|
+
type="number"
|
|
698
|
+
value={hourInputValue}
|
|
699
|
+
/>
|
|
700
|
+
{showHourDropdown && (
|
|
701
|
+
<div
|
|
702
|
+
className="time_dropdown"
|
|
703
|
+
ref={hourDropdownRef}
|
|
704
|
+
>
|
|
705
|
+
{generateHourOptions(timeFormat).map((h) => (
|
|
706
|
+
<div
|
|
707
|
+
className={`time_dropdown_option ${hour === h ? 'selected' : ''}`}
|
|
708
|
+
key={h}
|
|
709
|
+
onClick={() => handleHourOptionClick(h)}
|
|
710
|
+
>
|
|
711
|
+
{timeFormat === '24hour' ? h.toString().padStart(2, '0') : h}
|
|
712
|
+
</div>
|
|
713
|
+
))}
|
|
714
|
+
</div>
|
|
715
|
+
)}
|
|
716
|
+
</div>
|
|
717
|
+
<span className="time-separator">{':'}</span>
|
|
718
|
+
<div className="time_input_wrapper">
|
|
719
|
+
<label htmlFor={`${uniqueId}-minute`}>
|
|
720
|
+
<Caption
|
|
721
|
+
className="time_input_label"
|
|
722
|
+
size="sm"
|
|
723
|
+
text="Minute"
|
|
724
|
+
/>
|
|
725
|
+
</label>
|
|
726
|
+
<input
|
|
727
|
+
className={`time_input time-minute ${hasSelectedTime && !isCurrentTimeValid(hour, minute, meridiem) ? 'invalid' : ''}`}
|
|
728
|
+
id={`${uniqueId}-minute`}
|
|
729
|
+
inputMode="numeric"
|
|
730
|
+
max={59}
|
|
731
|
+
maxLength={2}
|
|
732
|
+
min={0}
|
|
733
|
+
name={`${uniqueId}-minute`}
|
|
734
|
+
onBlur={handleMinuteBlur}
|
|
735
|
+
onChange={handleMinuteChange}
|
|
736
|
+
onClick={() => { setShowMinuteDropdown(!showMinuteDropdown); setShowHourDropdown(false) }}
|
|
737
|
+
onKeyDown={handleMinuteKeyDown}
|
|
738
|
+
pattern="[0-9]*"
|
|
739
|
+
ref={minuteInputRef}
|
|
740
|
+
step={1}
|
|
741
|
+
tabIndex={0}
|
|
742
|
+
type="number"
|
|
743
|
+
value={minuteInputValue}
|
|
744
|
+
/>
|
|
745
|
+
{showMinuteDropdown && (
|
|
746
|
+
<div
|
|
747
|
+
className="time_dropdown"
|
|
748
|
+
ref={minuteDropdownRef}
|
|
749
|
+
>
|
|
750
|
+
{generateMinuteOptions().map((m) => (
|
|
751
|
+
<div
|
|
752
|
+
className={`time_dropdown_option ${minute === m ? 'selected' : ''}`}
|
|
753
|
+
key={m}
|
|
754
|
+
onClick={() => handleMinuteOptionClick(m)}
|
|
755
|
+
>
|
|
756
|
+
{m.toString().padStart(2, '0')}
|
|
757
|
+
</div>
|
|
758
|
+
))}
|
|
759
|
+
</div>
|
|
760
|
+
)}
|
|
761
|
+
</div>
|
|
762
|
+
{timeFormat === 'AMPM' && (
|
|
763
|
+
<div className="meridiem">
|
|
764
|
+
<Caption
|
|
765
|
+
className="time_input_label"
|
|
766
|
+
size="sm"
|
|
767
|
+
text="Period"
|
|
768
|
+
/>
|
|
769
|
+
<div className="pb_form_group_kit">
|
|
770
|
+
<SelectableCard
|
|
771
|
+
checked={meridiem === 'AM'}
|
|
772
|
+
className={!isAnyAMTimeValid() ? 'disabled_meridiem' : ''}
|
|
773
|
+
disabled={!isAnyAMTimeValid()}
|
|
774
|
+
inputId={`${uniqueId}-am`}
|
|
775
|
+
multi={false}
|
|
776
|
+
name={`${uniqueId}-meridiem`}
|
|
777
|
+
onChange={() => handleMeridiemChange('AM')}
|
|
778
|
+
text="AM"
|
|
779
|
+
value="AM"
|
|
780
|
+
/>
|
|
781
|
+
<SelectableCard
|
|
782
|
+
checked={meridiem === 'PM'}
|
|
783
|
+
className={!isAnyPMTimeValid() ? 'disabled_meridiem' : ''}
|
|
784
|
+
disabled={!isAnyPMTimeValid()}
|
|
785
|
+
inputId={`${uniqueId}-pm`}
|
|
786
|
+
multi={false}
|
|
787
|
+
name={`${uniqueId}-meridiem`}
|
|
788
|
+
onChange={() => handleMeridiemChange('PM')}
|
|
789
|
+
text="PM"
|
|
790
|
+
value="PM"
|
|
791
|
+
/>
|
|
792
|
+
</div>
|
|
793
|
+
</div>
|
|
794
|
+
)}
|
|
795
|
+
{/* Show validation error in dropdownwhen time is out of range */}
|
|
796
|
+
{hasSelectedTime && !isCurrentTimeValid(hour, minute, meridiem) && (
|
|
797
|
+
<div className="time_range_error">
|
|
798
|
+
<Caption
|
|
799
|
+
className="time_range_error_text"
|
|
800
|
+
marginTop="sm"
|
|
801
|
+
size="xs"
|
|
802
|
+
text={getTimeRangeErrorMessage(minTime, maxTime, timeFormat)}
|
|
803
|
+
/>
|
|
804
|
+
</div>
|
|
805
|
+
)}
|
|
806
|
+
{showTimezone && (
|
|
807
|
+
<Caption
|
|
808
|
+
lineHeight="tight"
|
|
809
|
+
marginTop="sm"
|
|
810
|
+
size="xs"
|
|
811
|
+
text={getTimezoneText()}
|
|
812
|
+
/>
|
|
813
|
+
)}
|
|
814
|
+
</div>
|
|
815
|
+
</div>
|
|
816
|
+
)}
|
|
817
|
+
</div>
|
|
818
|
+
</div>
|
|
819
|
+
)
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
export default TimePicker
|