@instructure/ui-time-select 8.34.1-snapshot-1 → 8.35.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 +3 -5
- package/es/TimeSelect/index.js +105 -53
- package/es/TimeSelect/props.js +4 -2
- package/lib/TimeSelect/index.js +105 -53
- package/lib/TimeSelect/props.js +4 -2
- package/package.json +13 -12
- package/src/TimeSelect/README.md +27 -1
- package/src/TimeSelect/index.tsx +90 -29
- package/src/TimeSelect/props.ts +42 -2
- package/tsconfig.build.json +1 -0
- package/tsconfig.build.tsbuildinfo +1 -1
- package/types/TimeSelect/index.d.ts +9 -2
- package/types/TimeSelect/index.d.ts.map +1 -1
- package/types/TimeSelect/props.d.ts +35 -0
- package/types/TimeSelect/props.d.ts.map +1 -1
package/src/TimeSelect/index.tsx
CHANGED
|
@@ -23,8 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
import React, { Component } from 'react'
|
|
26
|
-
import { Moment } from 'moment-timezone'
|
|
27
|
-
|
|
26
|
+
import type { Moment } from 'moment-timezone'
|
|
28
27
|
import { ApplyLocaleContext, Locale, DateTime } from '@instructure/ui-i18n'
|
|
29
28
|
import {
|
|
30
29
|
getInteraction,
|
|
@@ -32,7 +31,6 @@ import {
|
|
|
32
31
|
callRenderProp,
|
|
33
32
|
withDeterministicId
|
|
34
33
|
} from '@instructure/ui-react-utils'
|
|
35
|
-
|
|
36
34
|
import { testable } from '@instructure/ui-testable'
|
|
37
35
|
import { Select } from '@instructure/ui-select'
|
|
38
36
|
|
|
@@ -62,11 +60,11 @@ A component used to select a time value.
|
|
|
62
60
|
@withDeterministicId()
|
|
63
61
|
@testable()
|
|
64
62
|
class TimeSelect extends Component<TimeSelectProps, TimeSelectState> {
|
|
65
|
-
|
|
63
|
+
declare context: React.ContextType<typeof ApplyLocaleContext>
|
|
66
64
|
|
|
65
|
+
static readonly componentId = 'TimeSelect'
|
|
67
66
|
static allowedProps = allowedProps
|
|
68
67
|
static propTypes = propTypes
|
|
69
|
-
|
|
70
68
|
static defaultProps = {
|
|
71
69
|
defaultToFirstOption: false,
|
|
72
70
|
format: 'LT', // see https://momentjs.com/docs/#/displaying/
|
|
@@ -76,9 +74,9 @@ class TimeSelect extends Component<TimeSelectProps, TimeSelectState> {
|
|
|
76
74
|
visibleOptionsCount: 8,
|
|
77
75
|
placement: 'bottom stretch',
|
|
78
76
|
constrain: 'window',
|
|
79
|
-
renderEmptyOption: '---'
|
|
77
|
+
renderEmptyOption: '---',
|
|
78
|
+
allowNonStepInput: false
|
|
80
79
|
}
|
|
81
|
-
declare context: React.ContextType<typeof ApplyLocaleContext>
|
|
82
80
|
static contextType = ApplyLocaleContext
|
|
83
81
|
|
|
84
82
|
ref: Select | null = null
|
|
@@ -153,7 +151,6 @@ class TimeSelect extends Component<TimeSelectProps, TimeSelectState> {
|
|
|
153
151
|
// when uncontrolled, selection will be lost
|
|
154
152
|
this.setState(this.getInitialState())
|
|
155
153
|
}
|
|
156
|
-
|
|
157
154
|
if (this.props.value !== prevProps.value) {
|
|
158
155
|
const normalizedValue = this.normalizeISOTime(this.props.value)
|
|
159
156
|
// value changed
|
|
@@ -168,9 +165,12 @@ class TimeSelect extends Component<TimeSelectProps, TimeSelectState> {
|
|
|
168
165
|
const outsideVal = this.props.value ? this.props.value : ''
|
|
169
166
|
// value does not match an existing option
|
|
170
167
|
const date = DateTime.parse(outsideVal, this.locale(), this.timezone())
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
168
|
+
let label = ''
|
|
169
|
+
if (date.isValid()) {
|
|
170
|
+
label = this.props.format
|
|
171
|
+
? date.format(this.props.format)
|
|
172
|
+
: date.toISOString()
|
|
173
|
+
}
|
|
174
174
|
this.setState({
|
|
175
175
|
inputValue: option ? option.label : label,
|
|
176
176
|
selectedOptionId: option ? option.id : undefined
|
|
@@ -330,17 +330,21 @@ class TimeSelect extends Component<TimeSelectProps, TimeSelectState> {
|
|
|
330
330
|
handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
331
331
|
const value = event.target.value
|
|
332
332
|
const newOptions = this.filterOptions(value)
|
|
333
|
-
|
|
334
333
|
this.setState({
|
|
335
334
|
inputValue: value,
|
|
336
335
|
filteredOptions: newOptions,
|
|
337
336
|
highlightedOptionId: newOptions.length > 0 ? newOptions[0].id : undefined,
|
|
338
337
|
isShowingOptions: true
|
|
339
338
|
})
|
|
340
|
-
|
|
341
339
|
if (!this.state.isShowingOptions) {
|
|
342
340
|
this.props.onShowOptions?.(event)
|
|
343
341
|
}
|
|
342
|
+
const inputAsDate = this.parseInputText(value)
|
|
343
|
+
this.props.onInputChange?.(
|
|
344
|
+
event,
|
|
345
|
+
value,
|
|
346
|
+
inputAsDate.isValid() ? inputAsDate.toISOString() : undefined
|
|
347
|
+
)
|
|
344
348
|
}
|
|
345
349
|
|
|
346
350
|
handleShowOptions = (event: React.SyntheticEvent) => {
|
|
@@ -351,31 +355,42 @@ class TimeSelect extends Component<TimeSelectProps, TimeSelectState> {
|
|
|
351
355
|
this.props.onShowOptions?.(event)
|
|
352
356
|
}
|
|
353
357
|
|
|
354
|
-
handleHideOptions: SelectProps['onRequestHideOptions'] = (
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
const { selectedOptionId } = this.state
|
|
358
|
-
const option = this.getOption('id', selectedOptionId)
|
|
359
|
-
let prevValue = ''
|
|
360
|
-
|
|
358
|
+
handleHideOptions: SelectProps['onRequestHideOptions'] = (event) => {
|
|
359
|
+
const input = this.parseInputText(this.state.inputValue)
|
|
360
|
+
let defaultValue = ''
|
|
361
361
|
if (this.props.defaultValue) {
|
|
362
362
|
const date = DateTime.parse(
|
|
363
363
|
this.props.defaultValue,
|
|
364
364
|
this.locale(),
|
|
365
365
|
this.timezone()
|
|
366
366
|
)
|
|
367
|
-
|
|
367
|
+
defaultValue = this.props.format
|
|
368
368
|
? date.format(this.props.format)
|
|
369
369
|
: date.toISOString()
|
|
370
370
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
371
|
+
if (
|
|
372
|
+
!this.props.allowNonStepInput ||
|
|
373
|
+
(this.props.allowNonStepInput &&
|
|
374
|
+
input.isValid() &&
|
|
375
|
+
input.minute() % this.props.step! === 0)
|
|
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
|
+
}))
|
|
393
|
+
}
|
|
379
394
|
this.props.onHideOptions?.(event)
|
|
380
395
|
}
|
|
381
396
|
|
|
@@ -423,6 +438,9 @@ class TimeSelect extends Component<TimeSelectProps, TimeSelectState> {
|
|
|
423
438
|
}
|
|
424
439
|
|
|
425
440
|
if (id !== this.state.selectedOptionId) {
|
|
441
|
+
this.setState({
|
|
442
|
+
lastValidInput: newInputValue
|
|
443
|
+
})
|
|
426
444
|
this.props.onChange?.(event, {
|
|
427
445
|
value: option.value,
|
|
428
446
|
inputText: newInputValue
|
|
@@ -466,6 +484,46 @@ class TimeSelect extends Component<TimeSelectProps, TimeSelectState> {
|
|
|
466
484
|
)
|
|
467
485
|
}
|
|
468
486
|
|
|
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
|
+
parseInputText = (inputValue: string) => {
|
|
513
|
+
const input = DateTime.parse(
|
|
514
|
+
inputValue,
|
|
515
|
+
this.locale(),
|
|
516
|
+
this.timezone(),
|
|
517
|
+
[this.props.format!],
|
|
518
|
+
true
|
|
519
|
+
)
|
|
520
|
+
const baseDate = this.getBaseDate()
|
|
521
|
+
input.year(baseDate.year())
|
|
522
|
+
input.month(baseDate.month())
|
|
523
|
+
input.date(baseDate.date())
|
|
524
|
+
return input
|
|
525
|
+
}
|
|
526
|
+
|
|
469
527
|
render() {
|
|
470
528
|
const {
|
|
471
529
|
value,
|
|
@@ -490,6 +548,8 @@ class TimeSelect extends Component<TimeSelectProps, TimeSelectState> {
|
|
|
490
548
|
onFocus,
|
|
491
549
|
onShowOptions,
|
|
492
550
|
onHideOptions,
|
|
551
|
+
onInputChange,
|
|
552
|
+
onKeyDown,
|
|
493
553
|
mountNode,
|
|
494
554
|
...rest
|
|
495
555
|
} = this.props
|
|
@@ -524,6 +584,7 @@ class TimeSelect extends Component<TimeSelectProps, TimeSelectState> {
|
|
|
524
584
|
onRequestHighlightOption={this.handleHighlightOption}
|
|
525
585
|
onRequestSelectOption={this.handleSelectOption}
|
|
526
586
|
onInputChange={this.handleInputChange}
|
|
587
|
+
onKeyDown={this.onKeyDown}
|
|
527
588
|
{...passthroughProps(rest)}
|
|
528
589
|
>
|
|
529
590
|
{isShowingOptions && this.renderOptions()}
|
package/src/TimeSelect/props.ts
CHANGED
|
@@ -211,6 +211,32 @@ type TimeSelectOwnProps = {
|
|
|
211
211
|
* property.
|
|
212
212
|
*/
|
|
213
213
|
timezone?: string
|
|
214
|
+
/**
|
|
215
|
+
* Whether to allow the user to enter non-step divisible values in the input field.
|
|
216
|
+
* Note that even if this is set to `false` one can enter non-step divisible values programatically.
|
|
217
|
+
* The user will need to enter the value exactly (except for lower/uppercase) as specified by the `format` prop for
|
|
218
|
+
* it to be accepted.
|
|
219
|
+
*
|
|
220
|
+
* Default is `false`
|
|
221
|
+
*/
|
|
222
|
+
allowNonStepInput?: boolean
|
|
223
|
+
/**
|
|
224
|
+
* Callback fired when text input value changes.
|
|
225
|
+
*/
|
|
226
|
+
onInputChange?: (
|
|
227
|
+
/**
|
|
228
|
+
* The raw HTML input event
|
|
229
|
+
*/
|
|
230
|
+
event: React.ChangeEvent<HTMLInputElement>,
|
|
231
|
+
/**
|
|
232
|
+
* The text value in the input field.
|
|
233
|
+
*/
|
|
234
|
+
value: string,
|
|
235
|
+
/**
|
|
236
|
+
* Current value as ISO datetime string, undefined it its a non-valid value.
|
|
237
|
+
*/
|
|
238
|
+
valueAsISOString?: string
|
|
239
|
+
) => void
|
|
214
240
|
}
|
|
215
241
|
|
|
216
242
|
const propTypes: PropValidators<PropKeys> = {
|
|
@@ -243,7 +269,9 @@ const propTypes: PropValidators<PropKeys> = {
|
|
|
243
269
|
renderBeforeInput: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
|
|
244
270
|
renderAfterInput: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
|
|
245
271
|
locale: PropTypes.string,
|
|
246
|
-
timezone: PropTypes.string
|
|
272
|
+
timezone: PropTypes.string,
|
|
273
|
+
allowNonStepInput: PropTypes.bool,
|
|
274
|
+
onInputChange: PropTypes.func
|
|
247
275
|
}
|
|
248
276
|
|
|
249
277
|
const allowedProps: AllowedPropKeys = [
|
|
@@ -276,7 +304,9 @@ const allowedProps: AllowedPropKeys = [
|
|
|
276
304
|
'renderBeforeInput',
|
|
277
305
|
'renderAfterInput',
|
|
278
306
|
'locale',
|
|
279
|
-
'timezone'
|
|
307
|
+
'timezone',
|
|
308
|
+
'allowNonStepInput',
|
|
309
|
+
'onInputChange'
|
|
280
310
|
]
|
|
281
311
|
|
|
282
312
|
type TimeSelectOptions = {
|
|
@@ -286,12 +316,22 @@ type TimeSelectOptions = {
|
|
|
286
316
|
}
|
|
287
317
|
|
|
288
318
|
type TimeSelectState = {
|
|
319
|
+
/**
|
|
320
|
+
* The current value in the input field, not necessarily a valid time
|
|
321
|
+
*/
|
|
289
322
|
inputValue: string
|
|
290
323
|
options: TimeSelectOptions[]
|
|
291
324
|
filteredOptions: TimeSelectOptions[]
|
|
325
|
+
/**
|
|
326
|
+
* Whether to show the options list.
|
|
327
|
+
*/
|
|
292
328
|
isShowingOptions: boolean
|
|
293
329
|
highlightedOptionId?: string
|
|
294
330
|
selectedOptionId?: string
|
|
331
|
+
/**
|
|
332
|
+
* Last valid input when nonStepInput is true
|
|
333
|
+
*/
|
|
334
|
+
lastValidInput?: string
|
|
295
335
|
}
|
|
296
336
|
|
|
297
337
|
export type { TimeSelectProps, TimeSelectState, TimeSelectOptions }
|
package/tsconfig.build.json
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
{ "path": "../ui-select/tsconfig.build.json" },
|
|
16
16
|
{ "path": "../ui-test-locator/tsconfig.build.json" },
|
|
17
17
|
{ "path": "../ui-testable/tsconfig.build.json" },
|
|
18
|
+
{ "path": "../ui-utils/tsconfig.build.json" },
|
|
18
19
|
{ "path": "../ui-babel-preset/tsconfig.build.json" },
|
|
19
20
|
{ "path": "../ui-test-utils/tsconfig.build.json" },
|
|
20
21
|
{ "path": "../shared-types/tsconfig.build.json" }
|