@instructure/ui-time-select 11.7.2-snapshot-48 → 11.7.2-snapshot-49
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/CHANGELOG.md +12 -2
- package/es/TimeSelect/v2/index.js +535 -0
- package/es/TimeSelect/v2/props.js +26 -0
- package/es/exports/b.js +24 -0
- package/lib/TimeSelect/v2/index.js +546 -0
- package/lib/TimeSelect/v2/props.js +31 -0
- package/lib/exports/b.js +12 -0
- package/package.json +19 -19
- package/src/TimeSelect/v2/README.md +85 -0
- package/src/TimeSelect/v2/index.tsx +645 -0
- package/src/TimeSelect/v2/props.ts +327 -0
- package/src/exports/b.ts +26 -0
- package/tsconfig.build.tsbuildinfo +1 -1
- package/types/TimeSelect/v2/index.d.ts +109 -0
- package/types/TimeSelect/v2/index.d.ts.map +1 -0
- package/types/TimeSelect/v2/props.d.ts +249 -0
- package/types/TimeSelect/v2/props.d.ts.map +1 -0
- package/types/exports/b.d.ts +3 -0
- package/types/exports/b.d.ts.map +1 -0
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The MIT License (MIT)
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) 2015 - present Instructure, Inc.
|
|
5
|
+
*
|
|
6
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
* in the Software without restriction, including without limitation the rights
|
|
9
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
* furnished to do so, subject to the following conditions:
|
|
12
|
+
*
|
|
13
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
* copies or substantial portions of the Software.
|
|
15
|
+
*
|
|
16
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
* SOFTWARE.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { Component } from 'react'
|
|
26
|
+
import type { Moment } from 'moment-timezone'
|
|
27
|
+
import { ApplyLocaleContext, Locale, DateTime } from '@instructure/ui-i18n'
|
|
28
|
+
import {
|
|
29
|
+
getInteraction,
|
|
30
|
+
passthroughProps,
|
|
31
|
+
callRenderProp,
|
|
32
|
+
withDeterministicId
|
|
33
|
+
} from '@instructure/ui-react-utils'
|
|
34
|
+
import { Select } from '@instructure/ui-select/latest'
|
|
35
|
+
import * as utils from '@instructure/ui-utils'
|
|
36
|
+
|
|
37
|
+
import type { SelectProps } from '@instructure/ui-select/latest'
|
|
38
|
+
import type {
|
|
39
|
+
TimeSelectProps,
|
|
40
|
+
TimeSelectState,
|
|
41
|
+
TimeSelectOptions
|
|
42
|
+
} from './props'
|
|
43
|
+
|
|
44
|
+
import { allowedProps } from './props'
|
|
45
|
+
|
|
46
|
+
type GetOption = <F extends keyof TimeSelectOptions>(
|
|
47
|
+
field: F,
|
|
48
|
+
value?: TimeSelectOptions[F],
|
|
49
|
+
options?: TimeSelectOptions[]
|
|
50
|
+
) => TimeSelectOptions | undefined
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
---
|
|
54
|
+
category: components
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
A component used to select a time value.
|
|
58
|
+
**/
|
|
59
|
+
@withDeterministicId()
|
|
60
|
+
class TimeSelect extends Component<TimeSelectProps, TimeSelectState> {
|
|
61
|
+
declare context: React.ContextType<typeof ApplyLocaleContext>
|
|
62
|
+
|
|
63
|
+
static readonly componentId = 'TimeSelect'
|
|
64
|
+
static allowedProps = allowedProps
|
|
65
|
+
static defaultProps = {
|
|
66
|
+
defaultToFirstOption: false,
|
|
67
|
+
format: 'LT', // see https://momentjs.com/docs/#/displaying/
|
|
68
|
+
step: 30,
|
|
69
|
+
isRequired: false,
|
|
70
|
+
isInline: false,
|
|
71
|
+
visibleOptionsCount: 8,
|
|
72
|
+
placement: 'bottom stretch',
|
|
73
|
+
constrain: 'window',
|
|
74
|
+
renderEmptyOption: '---',
|
|
75
|
+
allowNonStepInput: false,
|
|
76
|
+
allowClearingSelection: false
|
|
77
|
+
}
|
|
78
|
+
static contextType = ApplyLocaleContext
|
|
79
|
+
|
|
80
|
+
ref: Select | null = null
|
|
81
|
+
|
|
82
|
+
private readonly _emptyOptionId =
|
|
83
|
+
this.props.deterministicId!('Select-EmptyOption')
|
|
84
|
+
|
|
85
|
+
constructor(props: TimeSelectProps) {
|
|
86
|
+
super(props)
|
|
87
|
+
this.state = this.getInitialState()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
componentDidMount() {
|
|
91
|
+
// we'll need to recalculate the state because the context value is
|
|
92
|
+
// set at this point (and it might change locale & timezone)
|
|
93
|
+
this.setState(this.getInitialState())
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
focus() {
|
|
97
|
+
this.ref?.focus()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
blur() {
|
|
101
|
+
this.ref && this.ref.blur()
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
get _select() {
|
|
105
|
+
console.warn(
|
|
106
|
+
'_select property is deprecated and will be removed in v9, please use ref instead'
|
|
107
|
+
)
|
|
108
|
+
return this.ref
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
get isControlled() {
|
|
112
|
+
return typeof this.props.value !== 'undefined'
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
get interaction() {
|
|
116
|
+
return getInteraction({ props: this.props })
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
get focused() {
|
|
120
|
+
return this.ref && this.ref.focused
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
get id() {
|
|
124
|
+
return this.ref && this.ref.id
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
locale() {
|
|
128
|
+
if (this.props.locale) {
|
|
129
|
+
return this.props.locale
|
|
130
|
+
} else if (this.context && this.context.locale) {
|
|
131
|
+
return this.context.locale
|
|
132
|
+
}
|
|
133
|
+
return Locale.browserLocale()
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
timezone() {
|
|
137
|
+
if (this.props.timezone) {
|
|
138
|
+
return this.props.timezone
|
|
139
|
+
} else if (this.context && this.context.timezone) {
|
|
140
|
+
return this.context.timezone
|
|
141
|
+
}
|
|
142
|
+
return DateTime.browserTimeZone()
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
componentDidUpdate(prevProps: TimeSelectProps) {
|
|
146
|
+
if (
|
|
147
|
+
this.props.step !== prevProps.step ||
|
|
148
|
+
this.props.format !== prevProps.format ||
|
|
149
|
+
this.props.locale !== prevProps.locale ||
|
|
150
|
+
this.props.timezone !== prevProps.timezone ||
|
|
151
|
+
this.props.allowNonStepInput !== prevProps.allowNonStepInput
|
|
152
|
+
) {
|
|
153
|
+
// options change, reset everything
|
|
154
|
+
// when controlled, selection will be preserved
|
|
155
|
+
// when uncontrolled, selection will be lost
|
|
156
|
+
this.setState(this.getInitialState())
|
|
157
|
+
}
|
|
158
|
+
if (this.props.value !== prevProps.value) {
|
|
159
|
+
let newValue: Moment | undefined
|
|
160
|
+
if (this.props.value) {
|
|
161
|
+
newValue = DateTime.parse(
|
|
162
|
+
this.props.value,
|
|
163
|
+
this.locale(),
|
|
164
|
+
this.timezone()
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
// value changed
|
|
168
|
+
const initState = this.getInitialState()
|
|
169
|
+
this.setState(initState)
|
|
170
|
+
// options need to be passed because state is not set immediately
|
|
171
|
+
let option
|
|
172
|
+
if (!this.isControlled) {
|
|
173
|
+
// preserve current value when changing from controlled to uncontrolled
|
|
174
|
+
if (prevProps.value) {
|
|
175
|
+
option = this.getOption(
|
|
176
|
+
'id',
|
|
177
|
+
this.getFormattedId(
|
|
178
|
+
DateTime.parse(prevProps.value, this.locale(), this.timezone())
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
} else if (newValue) {
|
|
183
|
+
option = this.getOption(
|
|
184
|
+
'id',
|
|
185
|
+
this.getFormattedId(newValue),
|
|
186
|
+
initState.options
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
const outsideVal = this.props.value ? this.props.value : ''
|
|
190
|
+
// value does not match an existing option
|
|
191
|
+
const date = DateTime.parse(outsideVal, this.locale(), this.timezone())
|
|
192
|
+
let label = ''
|
|
193
|
+
if (date.isValid()) {
|
|
194
|
+
label = this.props.format
|
|
195
|
+
? date.format(this.props.format)
|
|
196
|
+
: date.toISOString()
|
|
197
|
+
}
|
|
198
|
+
this.setState({
|
|
199
|
+
inputValue: option ? option.label : label,
|
|
200
|
+
selectedOptionId: option ? option.id : undefined
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
getFormattedId(date: Moment) {
|
|
206
|
+
// ISO8601 strings may contain a space. Remove any spaces before using the
|
|
207
|
+
// date as the id.
|
|
208
|
+
return date.toISOString().replace(/\s/g, '')
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
getInitialState(): TimeSelectState {
|
|
212
|
+
const initialOptions = this.generateOptions()
|
|
213
|
+
const initialSelection = this.getInitialOption(initialOptions)
|
|
214
|
+
return {
|
|
215
|
+
inputValue: initialSelection ? initialSelection.label : '',
|
|
216
|
+
options: initialOptions,
|
|
217
|
+
// 288 = 5 min step
|
|
218
|
+
filteredOptions:
|
|
219
|
+
initialOptions.length > 288
|
|
220
|
+
? initialOptions.filter(
|
|
221
|
+
(opt) => opt.value.minute() % this.props.step! === 0
|
|
222
|
+
)
|
|
223
|
+
: initialOptions,
|
|
224
|
+
isShowingOptions: false,
|
|
225
|
+
highlightedOptionId: initialSelection ? initialSelection.id : undefined,
|
|
226
|
+
selectedOptionId: initialSelection ? initialSelection.id : undefined,
|
|
227
|
+
isInputCleared: false
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
getInitialOption(options: TimeSelectOptions[]) {
|
|
232
|
+
const { value, defaultValue, defaultToFirstOption, format } = this.props
|
|
233
|
+
const initialValue = value || defaultValue
|
|
234
|
+
if (typeof initialValue === 'string') {
|
|
235
|
+
const date = DateTime.parse(initialValue, this.locale(), this.timezone())
|
|
236
|
+
// get option based on value or defaultValue, if provided
|
|
237
|
+
const option = this.getOption('value', date, options)
|
|
238
|
+
if (option) {
|
|
239
|
+
// value matches an existing option
|
|
240
|
+
return option
|
|
241
|
+
}
|
|
242
|
+
// value does not match an existing option
|
|
243
|
+
return {
|
|
244
|
+
id: this.getFormattedId(date),
|
|
245
|
+
label: format ? date.format(format) : date.toISOString(),
|
|
246
|
+
value: date
|
|
247
|
+
} as TimeSelectOptions
|
|
248
|
+
}
|
|
249
|
+
// otherwise, return first option, if desired
|
|
250
|
+
if (defaultToFirstOption) {
|
|
251
|
+
return options[0]
|
|
252
|
+
}
|
|
253
|
+
return undefined
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
getOption: GetOption = (field, value, options = this.state.options) => {
|
|
257
|
+
return options.find((option) => option[field] === value)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
getBaseDate() {
|
|
261
|
+
let baseDate
|
|
262
|
+
const baseValue = this.props.value || this.props.defaultValue
|
|
263
|
+
if (baseValue) {
|
|
264
|
+
baseDate = DateTime.parse(baseValue, this.locale(), this.timezone())
|
|
265
|
+
} else {
|
|
266
|
+
baseDate = DateTime.now(this.locale(), this.timezone())
|
|
267
|
+
}
|
|
268
|
+
return baseDate.set({ second: 0, millisecond: 0 }).clone()
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
generateOptions(): TimeSelectOptions[] {
|
|
272
|
+
const date = this.getBaseDate()
|
|
273
|
+
const options = []
|
|
274
|
+
const step = this.props.step ? this.props.step : 30
|
|
275
|
+
const maxMinute = this.props.allowNonStepInput ? 60 : 60 / step
|
|
276
|
+
const minuteStep = this.props.allowNonStepInput ? 1 : step
|
|
277
|
+
for (let hour = 0; hour < 24; hour++) {
|
|
278
|
+
for (let minute = 0; minute < maxMinute; minute++) {
|
|
279
|
+
const minutes = minute * minuteStep
|
|
280
|
+
const newDate = date.set({ hour: hour, minute: minutes })
|
|
281
|
+
// store time options
|
|
282
|
+
options.push({
|
|
283
|
+
id: this.getFormattedId(newDate),
|
|
284
|
+
value: newDate.clone(),
|
|
285
|
+
label: this.props.format
|
|
286
|
+
? newDate.format(this.props.format)
|
|
287
|
+
: newDate.toISOString()
|
|
288
|
+
})
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return options
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
filterOptions(inputValue: string) {
|
|
295
|
+
let inputNoSeconds = inputValue
|
|
296
|
+
// if the input contains seconds disregard them (e.g. if format = LTS)
|
|
297
|
+
if (inputValue.length > 5) {
|
|
298
|
+
// e.g. "5:34:"
|
|
299
|
+
const input = this.parseInputText(inputValue)
|
|
300
|
+
if (input.isValid()) {
|
|
301
|
+
input.set({ second: 0 })
|
|
302
|
+
inputNoSeconds = input.format(this.props.format)
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (this.props.allowNonStepInput && inputNoSeconds.length < 3) {
|
|
307
|
+
// could show too many results, show only step values
|
|
308
|
+
return this.state?.options.filter((option: TimeSelectOptions) => {
|
|
309
|
+
return (
|
|
310
|
+
option.label.toLowerCase().startsWith(inputNoSeconds.toLowerCase()) &&
|
|
311
|
+
option.value.minute() % this.props.step! == 0
|
|
312
|
+
)
|
|
313
|
+
})
|
|
314
|
+
}
|
|
315
|
+
return this.state?.options.filter((option: TimeSelectOptions) =>
|
|
316
|
+
option.label.toLowerCase().startsWith(inputNoSeconds.toLowerCase())
|
|
317
|
+
)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
handleRef = (node: Select) => {
|
|
321
|
+
this.ref = node
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
handleBlur = (event: React.FocusEvent<HTMLInputElement>) => {
|
|
325
|
+
this.props.onBlur?.(event)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
329
|
+
const value = event.target.value
|
|
330
|
+
const newOptions = this.filterOptions(value)
|
|
331
|
+
if (newOptions?.length == 1) {
|
|
332
|
+
// if there is only 1 option, it will be automatically selected except
|
|
333
|
+
// if in controlled mode (it would commit this change)
|
|
334
|
+
if (!this.isControlled) {
|
|
335
|
+
this.setState({ selectedOptionId: newOptions[0].id })
|
|
336
|
+
}
|
|
337
|
+
this.setState({ fireChangeOnBlur: newOptions[0] })
|
|
338
|
+
} else {
|
|
339
|
+
this.setState({
|
|
340
|
+
fireChangeOnBlur: undefined,
|
|
341
|
+
isInputCleared: this.props.allowClearingSelection! && value === ''
|
|
342
|
+
})
|
|
343
|
+
}
|
|
344
|
+
this.setState({
|
|
345
|
+
inputValue: value,
|
|
346
|
+
filteredOptions: newOptions,
|
|
347
|
+
highlightedOptionId: newOptions.length > 0 ? newOptions[0].id : undefined,
|
|
348
|
+
isShowingOptions: true
|
|
349
|
+
})
|
|
350
|
+
if (!this.state.isShowingOptions) {
|
|
351
|
+
this.props.onShowOptions?.(event)
|
|
352
|
+
}
|
|
353
|
+
const inputAsDate = this.parseInputText(value)
|
|
354
|
+
this.props.onInputChange?.(
|
|
355
|
+
event,
|
|
356
|
+
value,
|
|
357
|
+
inputAsDate.isValid() ? inputAsDate.toISOString() : undefined
|
|
358
|
+
)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
onKeyDown = (event: React.KeyboardEvent<any>) => {
|
|
362
|
+
const input = this.parseInputText(this.state.inputValue)
|
|
363
|
+
if (
|
|
364
|
+
event.key === 'Enter' &&
|
|
365
|
+
this.props.allowNonStepInput &&
|
|
366
|
+
input.isValid()
|
|
367
|
+
) {
|
|
368
|
+
this.setState(() => ({
|
|
369
|
+
isShowingOptions: false,
|
|
370
|
+
highlightedOptionId: undefined
|
|
371
|
+
}))
|
|
372
|
+
// others are set in handleBlurOrEsc
|
|
373
|
+
}
|
|
374
|
+
this.props.onKeyDown?.(event)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
handleShowOptions = (event: React.SyntheticEvent) => {
|
|
378
|
+
this.setState({
|
|
379
|
+
isShowingOptions: true,
|
|
380
|
+
highlightedOptionId: this.state.selectedOptionId
|
|
381
|
+
})
|
|
382
|
+
this.props.onShowOptions?.(event)
|
|
383
|
+
|
|
384
|
+
if (event.type.startsWith('key')) {
|
|
385
|
+
const keyboardEvent = event as React.KeyboardEvent
|
|
386
|
+
const children = this.state.filteredOptions
|
|
387
|
+
if (
|
|
388
|
+
!this.state.inputValue &&
|
|
389
|
+
children.length > 0 &&
|
|
390
|
+
!this.props.allowClearingSelection
|
|
391
|
+
) {
|
|
392
|
+
const optionId =
|
|
393
|
+
keyboardEvent.key === 'ArrowDown'
|
|
394
|
+
? children[0].id
|
|
395
|
+
: keyboardEvent.key === 'ArrowUp'
|
|
396
|
+
? children[children.length - 1].id
|
|
397
|
+
: undefined
|
|
398
|
+
optionId &&
|
|
399
|
+
this.setState({
|
|
400
|
+
highlightedOptionId: optionId
|
|
401
|
+
})
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Called when the input is blurred (=when clicking outside, tabbing away),
|
|
407
|
+
// when pressing ESC. NOT called when an item is selected via Enter/click,
|
|
408
|
+
// (but in this case it will be called later when the input is blurred.)
|
|
409
|
+
handleBlurOrEsc: SelectProps['onRequestHideOptions'] = (event) => {
|
|
410
|
+
const { selectedOptionId, inputValue, isInputCleared } = this.state
|
|
411
|
+
let defaultValue = ''
|
|
412
|
+
if (this.props.defaultValue) {
|
|
413
|
+
const date = DateTime.parse(
|
|
414
|
+
this.props.defaultValue,
|
|
415
|
+
this.locale(),
|
|
416
|
+
this.timezone()
|
|
417
|
+
)
|
|
418
|
+
defaultValue = this.props.format
|
|
419
|
+
? date.format(this.props.format)
|
|
420
|
+
: date.toISOString()
|
|
421
|
+
}
|
|
422
|
+
const selectedOption = this.getOption('id', selectedOptionId)
|
|
423
|
+
let newInputValue = defaultValue
|
|
424
|
+
// if input was completely cleared, ensure it stays clear
|
|
425
|
+
// e.g. defaultValue defined, but no selection yet made
|
|
426
|
+
if (inputValue === '' && this.props.allowClearingSelection) {
|
|
427
|
+
newInputValue = ''
|
|
428
|
+
} else if (selectedOption) {
|
|
429
|
+
// If there is a selected option use its value in the input field.
|
|
430
|
+
newInputValue = selectedOption.label
|
|
431
|
+
} else if (this.props.value) {
|
|
432
|
+
// If controlled and input is cleared and blurred after the first render, it should revert to value
|
|
433
|
+
const date = DateTime.parse(
|
|
434
|
+
this.props.value,
|
|
435
|
+
this.locale(),
|
|
436
|
+
this.timezone()
|
|
437
|
+
)
|
|
438
|
+
newInputValue = this.props.format
|
|
439
|
+
? date.format(this.props.format)
|
|
440
|
+
: date.toISOString()
|
|
441
|
+
}
|
|
442
|
+
this.setState(() => ({
|
|
443
|
+
isShowingOptions: false,
|
|
444
|
+
highlightedOptionId: undefined,
|
|
445
|
+
inputValue: newInputValue,
|
|
446
|
+
filteredOptions: this.filterOptions('')
|
|
447
|
+
}))
|
|
448
|
+
if (this.state.fireChangeOnBlur && (event as any).key !== 'Escape') {
|
|
449
|
+
this.setState(() => ({ fireChangeOnBlur: undefined }))
|
|
450
|
+
this.props.onChange?.(event, {
|
|
451
|
+
value: this.state.fireChangeOnBlur.value.toISOString(),
|
|
452
|
+
inputText: this.state.fireChangeOnBlur.label
|
|
453
|
+
})
|
|
454
|
+
} else if (
|
|
455
|
+
isInputCleared &&
|
|
456
|
+
(event as any).key !== 'Escape' &&
|
|
457
|
+
this.props.allowClearingSelection
|
|
458
|
+
) {
|
|
459
|
+
this.setState(() => ({ isInputCleared: false }))
|
|
460
|
+
this.props.onChange?.(event, {
|
|
461
|
+
value: '',
|
|
462
|
+
inputText: ''
|
|
463
|
+
})
|
|
464
|
+
}
|
|
465
|
+
// TODO only fire this if handleSelectOption was not called before.
|
|
466
|
+
this.props.onHideOptions?.(event)
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Called when an option is selected via mouse click or Enter.
|
|
470
|
+
handleSelectOption: SelectProps['onRequestSelectOption'] = (event, data) => {
|
|
471
|
+
if (data.id === this._emptyOptionId) {
|
|
472
|
+
this.setState({ isShowingOptions: false })
|
|
473
|
+
return
|
|
474
|
+
}
|
|
475
|
+
const selectedOption = this.getOption('id', data.id)
|
|
476
|
+
let newInputValue: string
|
|
477
|
+
const currentSelectedOptionId = this.state.selectedOptionId
|
|
478
|
+
|
|
479
|
+
// Focus needs to be reapplied to input
|
|
480
|
+
// after selecting an item to make sure VoiceOver behaves correctly on iOS
|
|
481
|
+
if (utils.isAndroidOrIOS()) {
|
|
482
|
+
this.blur()
|
|
483
|
+
this.focus()
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (this.isControlled) {
|
|
487
|
+
// in controlled mode we leave to the user to set the value of the
|
|
488
|
+
// component e.g. in the onChange event handler.
|
|
489
|
+
// This is accomplished by not setting a selectedOptionId
|
|
490
|
+
const prev = this.getOption('id', this.state.selectedOptionId)
|
|
491
|
+
newInputValue = prev ? prev.label : ''
|
|
492
|
+
this.setState({
|
|
493
|
+
isShowingOptions: false
|
|
494
|
+
})
|
|
495
|
+
} else {
|
|
496
|
+
newInputValue = selectedOption!.label
|
|
497
|
+
this.setState({
|
|
498
|
+
isShowingOptions: false,
|
|
499
|
+
selectedOptionId: data.id,
|
|
500
|
+
inputValue: newInputValue
|
|
501
|
+
})
|
|
502
|
+
}
|
|
503
|
+
if (data.id !== currentSelectedOptionId) {
|
|
504
|
+
this.props.onChange?.(event, {
|
|
505
|
+
value: selectedOption!.value.toISOString(),
|
|
506
|
+
inputText: newInputValue
|
|
507
|
+
})
|
|
508
|
+
}
|
|
509
|
+
this.props.onHideOptions?.(event)
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
handleHighlightOption: SelectProps['onRequestHighlightOption'] = (
|
|
513
|
+
_event,
|
|
514
|
+
data
|
|
515
|
+
) => {
|
|
516
|
+
if (data.id === this._emptyOptionId) return
|
|
517
|
+
|
|
518
|
+
this.setState((state) => ({
|
|
519
|
+
highlightedOptionId: data.id,
|
|
520
|
+
inputValue: state.inputValue
|
|
521
|
+
}))
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
renderOptions() {
|
|
525
|
+
const { filteredOptions, highlightedOptionId, selectedOptionId } =
|
|
526
|
+
this.state
|
|
527
|
+
|
|
528
|
+
if (filteredOptions.length < 1) {
|
|
529
|
+
return this.renderEmptyOption()
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
return filteredOptions.map((option: TimeSelectOptions) => {
|
|
533
|
+
const { id, label } = option
|
|
534
|
+
return (
|
|
535
|
+
<Select.Option
|
|
536
|
+
id={id}
|
|
537
|
+
key={id}
|
|
538
|
+
isHighlighted={id === highlightedOptionId}
|
|
539
|
+
isSelected={id === selectedOptionId}
|
|
540
|
+
>
|
|
541
|
+
{label}
|
|
542
|
+
</Select.Option>
|
|
543
|
+
)
|
|
544
|
+
})
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
renderEmptyOption() {
|
|
548
|
+
return (
|
|
549
|
+
<Select.Option
|
|
550
|
+
id={this._emptyOptionId}
|
|
551
|
+
isHighlighted={false}
|
|
552
|
+
isSelected={false}
|
|
553
|
+
>
|
|
554
|
+
{callRenderProp(this.props.renderEmptyOption)}
|
|
555
|
+
</Select.Option>
|
|
556
|
+
)
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
parseInputText = (inputValue: string) => {
|
|
560
|
+
const input = DateTime.parse(
|
|
561
|
+
inputValue,
|
|
562
|
+
this.locale(),
|
|
563
|
+
this.timezone(),
|
|
564
|
+
[this.props.format!],
|
|
565
|
+
true
|
|
566
|
+
)
|
|
567
|
+
const baseDate = this.getBaseDate()
|
|
568
|
+
input.year(baseDate.year())
|
|
569
|
+
input.month(baseDate.month())
|
|
570
|
+
input.date(baseDate.date())
|
|
571
|
+
return input
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
render() {
|
|
575
|
+
const {
|
|
576
|
+
value,
|
|
577
|
+
defaultValue,
|
|
578
|
+
placeholder,
|
|
579
|
+
renderLabel,
|
|
580
|
+
inputRef,
|
|
581
|
+
id,
|
|
582
|
+
listRef,
|
|
583
|
+
renderBeforeInput,
|
|
584
|
+
renderAfterInput,
|
|
585
|
+
isRequired,
|
|
586
|
+
isInline,
|
|
587
|
+
width,
|
|
588
|
+
format,
|
|
589
|
+
step,
|
|
590
|
+
optionsMaxWidth,
|
|
591
|
+
visibleOptionsCount,
|
|
592
|
+
messages,
|
|
593
|
+
placement,
|
|
594
|
+
constrain,
|
|
595
|
+
onFocus,
|
|
596
|
+
onShowOptions,
|
|
597
|
+
onHideOptions,
|
|
598
|
+
onInputChange,
|
|
599
|
+
onKeyDown,
|
|
600
|
+
mountNode,
|
|
601
|
+
...rest
|
|
602
|
+
} = this.props
|
|
603
|
+
|
|
604
|
+
const { inputValue, isShowingOptions } = this.state
|
|
605
|
+
return (
|
|
606
|
+
<Select
|
|
607
|
+
mountNode={mountNode}
|
|
608
|
+
renderLabel={renderLabel}
|
|
609
|
+
inputValue={inputValue}
|
|
610
|
+
interaction={this.interaction}
|
|
611
|
+
placeholder={placeholder}
|
|
612
|
+
id={id}
|
|
613
|
+
onFocus={onFocus}
|
|
614
|
+
onBlur={this.handleBlur}
|
|
615
|
+
ref={this.handleRef}
|
|
616
|
+
inputRef={inputRef}
|
|
617
|
+
listRef={listRef}
|
|
618
|
+
isRequired={isRequired}
|
|
619
|
+
isInline={isInline}
|
|
620
|
+
width={width}
|
|
621
|
+
optionsMaxWidth={optionsMaxWidth}
|
|
622
|
+
visibleOptionsCount={visibleOptionsCount}
|
|
623
|
+
messages={messages}
|
|
624
|
+
placement={placement}
|
|
625
|
+
constrain={constrain}
|
|
626
|
+
renderBeforeInput={renderBeforeInput}
|
|
627
|
+
renderAfterInput={renderAfterInput}
|
|
628
|
+
isShowingOptions={isShowingOptions}
|
|
629
|
+
onRequestShowOptions={this.handleShowOptions}
|
|
630
|
+
onRequestHideOptions={this.handleBlurOrEsc}
|
|
631
|
+
onRequestHighlightOption={this.handleHighlightOption}
|
|
632
|
+
onRequestSelectOption={this.handleSelectOption}
|
|
633
|
+
onInputChange={this.handleInputChange}
|
|
634
|
+
onKeyDown={this.onKeyDown}
|
|
635
|
+
{...passthroughProps(rest)}
|
|
636
|
+
data-cid="TimeSelect"
|
|
637
|
+
>
|
|
638
|
+
{isShowingOptions && this.renderOptions()}
|
|
639
|
+
</Select>
|
|
640
|
+
)
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
export { TimeSelect }
|
|
645
|
+
export default TimeSelect
|