@instructure/ui-time-select 8.37.1-snapshot-13 → 8.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1 -1
- package/es/TimeSelect/index.js +149 -151
- package/lib/TimeSelect/index.js +149 -151
- package/package.json +13 -13
- package/src/TimeSelect/README.md +1 -1
- package/src/TimeSelect/index.tsx +164 -164
- package/src/TimeSelect/props.ts +31 -7
- package/tsconfig.build.tsbuildinfo +1 -1
- package/types/TimeSelect/index.d.ts +7 -25
- package/types/TimeSelect/index.d.ts.map +1 -1
- package/types/TimeSelect/props.d.ts +28 -7
- package/types/TimeSelect/props.d.ts.map +1 -1
package/src/TimeSelect/index.tsx
CHANGED
|
@@ -80,6 +80,7 @@ class TimeSelect extends Component<TimeSelectProps, TimeSelectState> {
|
|
|
80
80
|
static contextType = ApplyLocaleContext
|
|
81
81
|
|
|
82
82
|
ref: Select | null = null
|
|
83
|
+
|
|
83
84
|
private readonly _emptyOptionId =
|
|
84
85
|
this.props.deterministicId!('Select-EmptyOption')
|
|
85
86
|
|
|
@@ -144,7 +145,8 @@ class TimeSelect extends Component<TimeSelectProps, TimeSelectState> {
|
|
|
144
145
|
this.props.step !== prevProps.step ||
|
|
145
146
|
this.props.format !== prevProps.format ||
|
|
146
147
|
this.props.locale !== prevProps.locale ||
|
|
147
|
-
this.props.timezone !== prevProps.timezone
|
|
148
|
+
this.props.timezone !== prevProps.timezone ||
|
|
149
|
+
this.props.allowNonStepInput !== prevProps.allowNonStepInput
|
|
148
150
|
) {
|
|
149
151
|
// options change, reset everything
|
|
150
152
|
// when controlled, selection will be preserved
|
|
@@ -152,15 +154,35 @@ class TimeSelect extends Component<TimeSelectProps, TimeSelectState> {
|
|
|
152
154
|
this.setState(this.getInitialState())
|
|
153
155
|
}
|
|
154
156
|
if (this.props.value !== prevProps.value) {
|
|
155
|
-
|
|
157
|
+
let newValue: Moment | undefined
|
|
158
|
+
if (this.props.value) {
|
|
159
|
+
newValue = DateTime.parse(
|
|
160
|
+
this.props.value,
|
|
161
|
+
this.locale(),
|
|
162
|
+
this.timezone()
|
|
163
|
+
)
|
|
164
|
+
}
|
|
156
165
|
// value changed
|
|
157
166
|
const initState = this.getInitialState()
|
|
158
167
|
this.setState(initState)
|
|
159
168
|
// options need to be passed because state is not set immediately
|
|
160
|
-
let option
|
|
161
|
-
if (
|
|
169
|
+
let option
|
|
170
|
+
if (!this.isControlled) {
|
|
162
171
|
// preserve current value when changing from controlled to uncontrolled
|
|
163
|
-
|
|
172
|
+
if (prevProps.value) {
|
|
173
|
+
option = this.getOption(
|
|
174
|
+
'id',
|
|
175
|
+
this.getFormattedId(
|
|
176
|
+
DateTime.parse(prevProps.value, this.locale(), this.timezone())
|
|
177
|
+
)
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
} else if (newValue) {
|
|
181
|
+
option = this.getOption(
|
|
182
|
+
'id',
|
|
183
|
+
this.getFormattedId(newValue),
|
|
184
|
+
initState.options
|
|
185
|
+
)
|
|
164
186
|
}
|
|
165
187
|
const outsideVal = this.props.value ? this.props.value : ''
|
|
166
188
|
// value does not match an existing option
|
|
@@ -178,13 +200,10 @@ class TimeSelect extends Component<TimeSelectProps, TimeSelectState> {
|
|
|
178
200
|
}
|
|
179
201
|
}
|
|
180
202
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
return value
|
|
186
|
-
}
|
|
187
|
-
return DateTime.parse(value, this.locale(), this.timezone()).toISOString()
|
|
203
|
+
getFormattedId(date: Moment) {
|
|
204
|
+
// ISO8601 strings may contain a space. Remove any spaces before using the
|
|
205
|
+
// date as the id.
|
|
206
|
+
return date.toISOString().replace(/\s/g, '')
|
|
188
207
|
}
|
|
189
208
|
|
|
190
209
|
getInitialState(): TimeSelectState {
|
|
@@ -193,33 +212,36 @@ class TimeSelect extends Component<TimeSelectProps, TimeSelectState> {
|
|
|
193
212
|
return {
|
|
194
213
|
inputValue: initialSelection ? initialSelection.label : '',
|
|
195
214
|
options: initialOptions,
|
|
196
|
-
|
|
215
|
+
// 288 = 5 min step
|
|
216
|
+
filteredOptions:
|
|
217
|
+
initialOptions.length > 288
|
|
218
|
+
? initialOptions.filter(
|
|
219
|
+
(opt) => opt.value.minute() % this.props.step! === 0
|
|
220
|
+
)
|
|
221
|
+
: initialOptions,
|
|
197
222
|
isShowingOptions: false,
|
|
198
223
|
highlightedOptionId: initialSelection ? initialSelection.id : undefined,
|
|
199
224
|
selectedOptionId: initialSelection ? initialSelection.id : undefined
|
|
200
225
|
}
|
|
201
226
|
}
|
|
202
227
|
|
|
203
|
-
getInitialOption(
|
|
204
|
-
options: TimeSelectOptions[]
|
|
205
|
-
): TimeSelectOptions | undefined {
|
|
228
|
+
getInitialOption(options: TimeSelectOptions[]) {
|
|
206
229
|
const { value, defaultValue, defaultToFirstOption, format } = this.props
|
|
207
230
|
const initialValue = value || defaultValue
|
|
208
|
-
|
|
209
231
|
if (typeof initialValue === 'string') {
|
|
232
|
+
const date = DateTime.parse(initialValue, this.locale(), this.timezone())
|
|
210
233
|
// get option based on value or defaultValue, if provided
|
|
211
|
-
const option = this.getOption(
|
|
212
|
-
'value',
|
|
213
|
-
this.normalizeISOTime(initialValue),
|
|
214
|
-
options
|
|
215
|
-
)
|
|
234
|
+
const option = this.getOption('value', date, options)
|
|
216
235
|
if (option) {
|
|
217
236
|
// value matches an existing option
|
|
218
237
|
return option
|
|
219
238
|
}
|
|
220
239
|
// value does not match an existing option
|
|
221
|
-
|
|
222
|
-
|
|
240
|
+
return {
|
|
241
|
+
id: this.getFormattedId(date),
|
|
242
|
+
label: format ? date.format(format) : date.toISOString(),
|
|
243
|
+
value: date
|
|
244
|
+
} as TimeSelectOptions
|
|
223
245
|
}
|
|
224
246
|
// otherwise, return first option, if desired
|
|
225
247
|
if (defaultToFirstOption) {
|
|
@@ -232,12 +254,6 @@ class TimeSelect extends Component<TimeSelectProps, TimeSelectState> {
|
|
|
232
254
|
return options.find((option) => option[field] === value)
|
|
233
255
|
}
|
|
234
256
|
|
|
235
|
-
getFormattedId(date: Moment) {
|
|
236
|
-
// ISO8601 strings may contain a space. Remove any spaces before using the
|
|
237
|
-
// date as the id.
|
|
238
|
-
return date.toISOString().replace(/\s/g, '')
|
|
239
|
-
}
|
|
240
|
-
|
|
241
257
|
getBaseDate() {
|
|
242
258
|
let baseDate
|
|
243
259
|
const baseValue = this.props.value || this.props.defaultValue
|
|
@@ -249,18 +265,20 @@ class TimeSelect extends Component<TimeSelectProps, TimeSelectState> {
|
|
|
249
265
|
return baseDate.set({ second: 0, millisecond: 0 }).clone()
|
|
250
266
|
}
|
|
251
267
|
|
|
252
|
-
generateOptions() {
|
|
268
|
+
generateOptions(): TimeSelectOptions[] {
|
|
253
269
|
const date = this.getBaseDate()
|
|
254
270
|
const options = []
|
|
255
|
-
|
|
271
|
+
const step = this.props.step ? this.props.step : 30
|
|
272
|
+
const maxMinute = this.props.allowNonStepInput ? 60 : 60 / step
|
|
273
|
+
const minuteStep = this.props.allowNonStepInput ? 1 : step
|
|
256
274
|
for (let hour = 0; hour < 24; hour++) {
|
|
257
|
-
for (let minute = 0; minute <
|
|
258
|
-
const minutes = minute *
|
|
275
|
+
for (let minute = 0; minute < maxMinute; minute++) {
|
|
276
|
+
const minutes = minute * minuteStep
|
|
259
277
|
const newDate = date.set({ hour: hour, minute: minutes })
|
|
260
278
|
// store time options
|
|
261
279
|
options.push({
|
|
262
|
-
id: this.getFormattedId(newDate),
|
|
263
|
-
value: newDate.
|
|
280
|
+
id: this.getFormattedId(newDate),
|
|
281
|
+
value: newDate.clone(),
|
|
264
282
|
label: this.props.format
|
|
265
283
|
? newDate.format(this.props.format)
|
|
266
284
|
: newDate.toISOString()
|
|
@@ -271,51 +289,29 @@ class TimeSelect extends Component<TimeSelectProps, TimeSelectState> {
|
|
|
271
289
|
}
|
|
272
290
|
|
|
273
291
|
filterOptions(inputValue: string) {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
)
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
filteredOptions,
|
|
283
|
-
highlightedOptionId,
|
|
284
|
-
selectedOptionId
|
|
285
|
-
} = this.state
|
|
286
|
-
|
|
287
|
-
// an option matching user input exists
|
|
288
|
-
if (filteredOptions.length === 1) {
|
|
289
|
-
const onlyOption = filteredOptions[0]
|
|
290
|
-
// automatically select the matching option
|
|
291
|
-
if (onlyOption.label.toLowerCase() === inputValue.toLowerCase()) {
|
|
292
|
-
return {
|
|
293
|
-
inputValue: onlyOption.label,
|
|
294
|
-
selectedOptionId: onlyOption.id,
|
|
295
|
-
filteredOptions: this.filterOptions('')
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
// no match found, return selected option label to input
|
|
300
|
-
const selectedOption = this.getOption('id', selectedOptionId)
|
|
301
|
-
if (selectedOption) {
|
|
302
|
-
return { inputValue: selectedOption.label }
|
|
303
|
-
}
|
|
304
|
-
// input value is from highlighted option, not user input
|
|
305
|
-
if (highlightedOptionId) {
|
|
306
|
-
if (inputValue === this.getOption('id', highlightedOptionId)!.label) {
|
|
307
|
-
return {
|
|
308
|
-
inputValue: '',
|
|
309
|
-
filteredOptions: this.filterOptions('')
|
|
310
|
-
}
|
|
292
|
+
let inputNoSeconds = inputValue
|
|
293
|
+
// if the input contains seconds disregard them (e.g. if format = LTS)
|
|
294
|
+
if (inputValue.length > 5) {
|
|
295
|
+
// e.g. "5:34:"
|
|
296
|
+
const input = this.parseInputText(inputValue)
|
|
297
|
+
if (input.isValid()) {
|
|
298
|
+
input.set({ second: 0 })
|
|
299
|
+
inputNoSeconds = input.format(this.props.format)
|
|
311
300
|
}
|
|
312
301
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
return
|
|
302
|
+
|
|
303
|
+
if (this.props.allowNonStepInput && inputNoSeconds.length < 3) {
|
|
304
|
+
// could show too many results, show only step values
|
|
305
|
+
return this.state?.options.filter((option: TimeSelectOptions) => {
|
|
306
|
+
return (
|
|
307
|
+
option.label.toLowerCase().startsWith(inputNoSeconds.toLowerCase()) &&
|
|
308
|
+
option.value.minute() % this.props.step! == 0
|
|
309
|
+
)
|
|
310
|
+
})
|
|
317
311
|
}
|
|
318
|
-
return
|
|
312
|
+
return this.state?.options.filter((option: TimeSelectOptions) =>
|
|
313
|
+
option.label.toLowerCase().startsWith(inputNoSeconds.toLowerCase())
|
|
314
|
+
)
|
|
319
315
|
}
|
|
320
316
|
|
|
321
317
|
handleRef = (node: Select) => {
|
|
@@ -323,13 +319,29 @@ class TimeSelect extends Component<TimeSelectProps, TimeSelectState> {
|
|
|
323
319
|
}
|
|
324
320
|
|
|
325
321
|
handleBlur = (event: React.FocusEvent<HTMLInputElement>) => {
|
|
326
|
-
this.setState({ highlightedOptionId: undefined })
|
|
327
322
|
this.props.onBlur?.(event)
|
|
328
323
|
}
|
|
329
324
|
|
|
330
325
|
handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
331
326
|
const value = event.target.value
|
|
332
327
|
const newOptions = this.filterOptions(value)
|
|
328
|
+
if (newOptions?.length == 1) {
|
|
329
|
+
// if there is only 1 option, it will be automatically selected except
|
|
330
|
+
// if in controlled mode (it would commit this change)
|
|
331
|
+
if (!this.isControlled) {
|
|
332
|
+
this.setState({ selectedOptionId: newOptions[0].id })
|
|
333
|
+
}
|
|
334
|
+
this.setState({ fireChangeOnBlur: newOptions[0] })
|
|
335
|
+
} else {
|
|
336
|
+
this.setState({
|
|
337
|
+
// needs not to lose selectedOptionId in controlled mode otherwise it'd
|
|
338
|
+
// revert to the default or '' instead of the set value
|
|
339
|
+
selectedOptionId: this.isControlled
|
|
340
|
+
? this.state.selectedOptionId
|
|
341
|
+
: undefined,
|
|
342
|
+
fireChangeOnBlur: undefined
|
|
343
|
+
})
|
|
344
|
+
}
|
|
333
345
|
this.setState({
|
|
334
346
|
inputValue: value,
|
|
335
347
|
filteredOptions: newOptions,
|
|
@@ -347,6 +359,22 @@ class TimeSelect extends Component<TimeSelectProps, TimeSelectState> {
|
|
|
347
359
|
)
|
|
348
360
|
}
|
|
349
361
|
|
|
362
|
+
onKeyDown = (event: React.KeyboardEvent<any>) => {
|
|
363
|
+
const input = this.parseInputText(this.state.inputValue)
|
|
364
|
+
if (
|
|
365
|
+
event.key === 'Enter' &&
|
|
366
|
+
this.props.allowNonStepInput &&
|
|
367
|
+
input.isValid()
|
|
368
|
+
) {
|
|
369
|
+
this.setState(() => ({
|
|
370
|
+
isShowingOptions: false,
|
|
371
|
+
highlightedOptionId: undefined
|
|
372
|
+
}))
|
|
373
|
+
// others are set in handleBlurOrEsc
|
|
374
|
+
}
|
|
375
|
+
this.props.onKeyDown?.(event)
|
|
376
|
+
}
|
|
377
|
+
|
|
350
378
|
handleShowOptions = (event: React.SyntheticEvent) => {
|
|
351
379
|
this.setState({
|
|
352
380
|
isShowingOptions: true,
|
|
@@ -355,8 +383,11 @@ class TimeSelect extends Component<TimeSelectProps, TimeSelectState> {
|
|
|
355
383
|
this.props.onShowOptions?.(event)
|
|
356
384
|
}
|
|
357
385
|
|
|
358
|
-
|
|
359
|
-
|
|
386
|
+
// Called when the input is blurred (=when clicking outside, tabbing away),
|
|
387
|
+
// when pressing ESC. NOT called when an item is selected via Enter/click,
|
|
388
|
+
// (but in this case it will be called later when the input is blurred.)
|
|
389
|
+
handleBlurOrEsc: SelectProps['onRequestHideOptions'] = (event) => {
|
|
390
|
+
const { selectedOptionId, inputValue } = this.state
|
|
360
391
|
let defaultValue = ''
|
|
361
392
|
if (this.props.defaultValue) {
|
|
362
393
|
const date = DateTime.parse(
|
|
@@ -368,87 +399,81 @@ class TimeSelect extends Component<TimeSelectProps, TimeSelectState> {
|
|
|
368
399
|
? date.format(this.props.format)
|
|
369
400
|
: date.toISOString()
|
|
370
401
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
) {
|
|
377
|
-
const { selectedOptionId } = this.state
|
|
378
|
-
const option = this.getOption('id', selectedOptionId)
|
|
379
|
-
this.setState(() => ({
|
|
380
|
-
isShowingOptions: false,
|
|
381
|
-
highlightedOptionId: undefined,
|
|
382
|
-
inputValue: selectedOptionId ? option!.label : defaultValue,
|
|
383
|
-
filteredOptions: this.filterOptions(''),
|
|
384
|
-
...this.matchValue()
|
|
385
|
-
}))
|
|
386
|
-
} else {
|
|
387
|
-
this.setState(() => ({
|
|
388
|
-
isShowingOptions: false,
|
|
389
|
-
highlightedOptionId: undefined,
|
|
390
|
-
inputValue: this.state.lastValidInput || defaultValue || '',
|
|
391
|
-
filteredOptions: this.filterOptions('')
|
|
392
|
-
}))
|
|
402
|
+
const selectedOption = this.getOption('id', selectedOptionId)
|
|
403
|
+
let newInputValue = defaultValue
|
|
404
|
+
if (selectedOption) {
|
|
405
|
+
// If there is a selected option use its value in the input field.
|
|
406
|
+
newInputValue = selectedOption.label
|
|
393
407
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
this.setState((state) => ({
|
|
406
|
-
highlightedOptionId: id,
|
|
407
|
-
inputValue: type === 'keydown' ? option : state.inputValue
|
|
408
|
+
// if input was completely cleared, ensure it stays clear
|
|
409
|
+
// e.g. defaultValue defined, but no selection yet made
|
|
410
|
+
else if (inputValue === '') {
|
|
411
|
+
newInputValue = ''
|
|
412
|
+
}
|
|
413
|
+
this.setState(() => ({
|
|
414
|
+
isShowingOptions: false,
|
|
415
|
+
highlightedOptionId: undefined,
|
|
416
|
+
inputValue: newInputValue,
|
|
417
|
+
filteredOptions: this.filterOptions('')
|
|
408
418
|
}))
|
|
419
|
+
if (this.state.fireChangeOnBlur && (event as any).key !== 'Escape') {
|
|
420
|
+
this.setState(() => ({ fireChangeOnBlur: undefined }))
|
|
421
|
+
this.props.onChange?.(event, {
|
|
422
|
+
value: this.state.fireChangeOnBlur.value.toISOString(),
|
|
423
|
+
inputText: this.state.fireChangeOnBlur.label
|
|
424
|
+
})
|
|
425
|
+
}
|
|
426
|
+
// TODO only fire this if handleSelectOption was not called before.
|
|
427
|
+
this.props.onHideOptions?.(event)
|
|
409
428
|
}
|
|
410
429
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
) => {
|
|
415
|
-
if (id === this._emptyOptionId) {
|
|
430
|
+
// Called when an option is selected via mouse click or Enter.
|
|
431
|
+
handleSelectOption: SelectProps['onRequestSelectOption'] = (event, data) => {
|
|
432
|
+
if (data.id === this._emptyOptionId) {
|
|
416
433
|
this.setState({ isShowingOptions: false })
|
|
417
434
|
return
|
|
418
435
|
}
|
|
419
|
-
const
|
|
420
|
-
|
|
436
|
+
const selectedOption = this.getOption('id', data.id)
|
|
421
437
|
let newInputValue: string
|
|
438
|
+
const currentSelectedOptionId = this.state.selectedOptionId
|
|
422
439
|
if (this.isControlled) {
|
|
440
|
+
// in controlled mode we leave to the user to set the value of the
|
|
441
|
+
// component e.g. in the onChange event handler.
|
|
442
|
+
// This is accomplished by not setting a selectedOptionId
|
|
423
443
|
const prev = this.getOption('id', this.state.selectedOptionId)
|
|
424
444
|
newInputValue = prev ? prev.label : ''
|
|
425
445
|
this.setState({
|
|
426
|
-
isShowingOptions: false
|
|
427
|
-
inputValue: newInputValue,
|
|
428
|
-
filteredOptions: this.filterOptions('')
|
|
446
|
+
isShowingOptions: false
|
|
429
447
|
})
|
|
430
448
|
} else {
|
|
431
|
-
newInputValue =
|
|
449
|
+
newInputValue = selectedOption!.label
|
|
432
450
|
this.setState({
|
|
433
451
|
isShowingOptions: false,
|
|
434
|
-
selectedOptionId: id,
|
|
435
|
-
inputValue: newInputValue
|
|
436
|
-
filteredOptions: this.filterOptions('')
|
|
452
|
+
selectedOptionId: data.id,
|
|
453
|
+
inputValue: newInputValue
|
|
437
454
|
})
|
|
438
455
|
}
|
|
439
|
-
|
|
440
|
-
if (id !== this.state.selectedOptionId) {
|
|
441
|
-
this.setState({
|
|
442
|
-
lastValidInput: newInputValue
|
|
443
|
-
})
|
|
456
|
+
if (data.id !== currentSelectedOptionId) {
|
|
444
457
|
this.props.onChange?.(event, {
|
|
445
|
-
value:
|
|
458
|
+
value: selectedOption!.value.toISOString(),
|
|
446
459
|
inputText: newInputValue
|
|
447
460
|
})
|
|
448
461
|
}
|
|
449
462
|
this.props.onHideOptions?.(event)
|
|
450
463
|
}
|
|
451
464
|
|
|
465
|
+
handleHighlightOption: SelectProps['onRequestHighlightOption'] = (
|
|
466
|
+
event,
|
|
467
|
+
data
|
|
468
|
+
) => {
|
|
469
|
+
if (data.id === this._emptyOptionId) return
|
|
470
|
+
const option = this.getOption('id', data.id)!.label
|
|
471
|
+
this.setState((state) => ({
|
|
472
|
+
highlightedOptionId: data.id,
|
|
473
|
+
inputValue: event.type === 'keydown' ? option : state.inputValue
|
|
474
|
+
}))
|
|
475
|
+
}
|
|
476
|
+
|
|
452
477
|
renderOptions() {
|
|
453
478
|
const { filteredOptions, highlightedOptionId, selectedOptionId } =
|
|
454
479
|
this.state
|
|
@@ -461,7 +486,7 @@ class TimeSelect extends Component<TimeSelectProps, TimeSelectState> {
|
|
|
461
486
|
const { id, label } = option
|
|
462
487
|
return (
|
|
463
488
|
<Select.Option
|
|
464
|
-
id={id
|
|
489
|
+
id={id}
|
|
465
490
|
key={id}
|
|
466
491
|
isHighlighted={id === highlightedOptionId}
|
|
467
492
|
isSelected={id === selectedOptionId}
|
|
@@ -484,31 +509,6 @@ class TimeSelect extends Component<TimeSelectProps, TimeSelectState> {
|
|
|
484
509
|
)
|
|
485
510
|
}
|
|
486
511
|
|
|
487
|
-
onKeyDown = (event: React.KeyboardEvent<any>) => {
|
|
488
|
-
const input = this.parseInputText(this.state.inputValue)
|
|
489
|
-
// validate input and if OK close dropdown
|
|
490
|
-
if (
|
|
491
|
-
event.key === 'Enter' &&
|
|
492
|
-
this.props.allowNonStepInput &&
|
|
493
|
-
input.isValid() &&
|
|
494
|
-
input.minute() % this.props.step! !== 0 &&
|
|
495
|
-
this.state.lastValidInput != this.state.inputValue
|
|
496
|
-
) {
|
|
497
|
-
this.setState(() => ({
|
|
498
|
-
isShowingOptions: false,
|
|
499
|
-
highlightedOptionId: undefined,
|
|
500
|
-
filteredOptions: this.filterOptions(''),
|
|
501
|
-
lastValidInput: this.state.inputValue
|
|
502
|
-
}))
|
|
503
|
-
this.props.onChange?.(event, {
|
|
504
|
-
value: input.toISOString(),
|
|
505
|
-
inputText: this.state.inputValue
|
|
506
|
-
})
|
|
507
|
-
// others are set in OnHideOptions
|
|
508
|
-
}
|
|
509
|
-
this.props.onKeyDown?.(event)
|
|
510
|
-
}
|
|
511
|
-
|
|
512
512
|
parseInputText = (inputValue: string) => {
|
|
513
513
|
const input = DateTime.parse(
|
|
514
514
|
inputValue,
|
|
@@ -580,7 +580,7 @@ class TimeSelect extends Component<TimeSelectProps, TimeSelectState> {
|
|
|
580
580
|
renderAfterInput={renderAfterInput}
|
|
581
581
|
isShowingOptions={isShowingOptions}
|
|
582
582
|
onRequestShowOptions={this.handleShowOptions}
|
|
583
|
-
onRequestHideOptions={this.
|
|
583
|
+
onRequestHideOptions={this.handleBlurOrEsc}
|
|
584
584
|
onRequestHighlightOption={this.handleHighlightOption}
|
|
585
585
|
onRequestSelectOption={this.handleSelectOption}
|
|
586
586
|
onInputChange={this.handleInputChange}
|
package/src/TimeSelect/props.ts
CHANGED
|
@@ -42,6 +42,7 @@ import type {
|
|
|
42
42
|
} from '@instructure/ui-position'
|
|
43
43
|
import type { WithDeterministicIdProps } from '@instructure/ui-react-utils'
|
|
44
44
|
import { Renderable } from '@instructure/shared-types'
|
|
45
|
+
import type { Moment } from 'moment-timezone'
|
|
45
46
|
|
|
46
47
|
type PropKeys = keyof TimeSelectOwnProps
|
|
47
48
|
|
|
@@ -78,6 +79,9 @@ type TimeSelectOwnProps = {
|
|
|
78
79
|
id?: string
|
|
79
80
|
/**
|
|
80
81
|
* The format to use when displaying the possible and currently selected options.
|
|
82
|
+
* This component currently rounds seconds down to the minute.
|
|
83
|
+
* Defaults to `LT`, which is localized time without seconds, e.g. "16:45" or
|
|
84
|
+
* "4:45 PM"
|
|
81
85
|
*
|
|
82
86
|
* See [moment](https://momentjs.com/docs/#/displaying/format/) for the list
|
|
83
87
|
* of available formats.
|
|
@@ -141,13 +145,16 @@ type TimeSelectOwnProps = {
|
|
|
141
145
|
*/
|
|
142
146
|
mountNode?: PositionMountNode
|
|
143
147
|
/**
|
|
144
|
-
* Callback fired when a new option is selected.
|
|
148
|
+
* Callback fired when a new option is selected. This can happen in the
|
|
149
|
+
* following ways:
|
|
150
|
+
* 1. User clicks/presses enter on an option in the dropdown and focuses away
|
|
151
|
+
* 2. User enters a valid time manually and focuses away
|
|
145
152
|
* @param event - the event object
|
|
146
|
-
* @param data - additional data
|
|
153
|
+
* @param data - additional data containing the value and the input string
|
|
147
154
|
*/
|
|
148
155
|
onChange?: (
|
|
149
156
|
event: React.SyntheticEvent,
|
|
150
|
-
data: { value
|
|
157
|
+
data: { value: string; inputText: string }
|
|
151
158
|
) => void
|
|
152
159
|
/**
|
|
153
160
|
* Callback fired when text input receives focus.
|
|
@@ -310,8 +317,11 @@ const allowedProps: AllowedPropKeys = [
|
|
|
310
317
|
]
|
|
311
318
|
|
|
312
319
|
type TimeSelectOptions = {
|
|
313
|
-
|
|
314
|
-
|
|
320
|
+
// the ID of this option, ISO date without spaces
|
|
321
|
+
id: string
|
|
322
|
+
// the actual date value
|
|
323
|
+
value: Moment
|
|
324
|
+
// the label shown to the user
|
|
315
325
|
label: string
|
|
316
326
|
}
|
|
317
327
|
|
|
@@ -320,18 +330,32 @@ type TimeSelectState = {
|
|
|
320
330
|
* The current value in the input field, not necessarily a valid time
|
|
321
331
|
*/
|
|
322
332
|
inputValue: string
|
|
333
|
+
/**
|
|
334
|
+
* All possible options. Filtered down because if `allowNonStepInput` is true
|
|
335
|
+
* it'd be 24*60 options and filtered by user input.
|
|
336
|
+
*/
|
|
323
337
|
options: TimeSelectOptions[]
|
|
338
|
+
/**
|
|
339
|
+
* The options shown in the options list.
|
|
340
|
+
*/
|
|
324
341
|
filteredOptions: TimeSelectOptions[]
|
|
325
342
|
/**
|
|
326
343
|
* Whether to show the options list.
|
|
327
344
|
*/
|
|
328
345
|
isShowingOptions: boolean
|
|
346
|
+
/**
|
|
347
|
+
* The highlighted option in the dropdown e.g. by hovering,
|
|
348
|
+
* not necessarily selected
|
|
349
|
+
*/
|
|
329
350
|
highlightedOptionId?: string
|
|
351
|
+
/**
|
|
352
|
+
* The ID of the selected option in the options list dropdown
|
|
353
|
+
*/
|
|
330
354
|
selectedOptionId?: string
|
|
331
355
|
/**
|
|
332
|
-
*
|
|
356
|
+
* fire onChange event when the popup closes?
|
|
333
357
|
*/
|
|
334
|
-
|
|
358
|
+
fireChangeOnBlur?: TimeSelectOptions
|
|
335
359
|
}
|
|
336
360
|
|
|
337
361
|
export type { TimeSelectProps, TimeSelectState, TimeSelectOptions }
|