@instructure/ui-date-time-input 11.7.3-snapshot-29 → 11.7.3-snapshot-30
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 +27 -1
- package/es/DateTimeInput/v2/index.js +343 -0
- package/es/DateTimeInput/v2/props.js +26 -0
- package/es/exports/b.js +24 -0
- package/lib/DateTimeInput/v2/index.js +349 -0
- package/lib/DateTimeInput/v2/props.js +31 -0
- package/lib/exports/b.js +12 -0
- package/package.json +24 -24
- package/src/DateTimeInput/v2/README.md +261 -0
- package/src/DateTimeInput/v2/index.tsx +439 -0
- package/src/DateTimeInput/v2/props.ts +299 -0
- package/src/exports/b.ts +25 -0
- package/tsconfig.build.tsbuildinfo +1 -1
- package/types/DateTimeInput/v2/index.d.ts +52 -0
- package/types/DateTimeInput/v2/index.d.ts.map +1 -0
- package/types/DateTimeInput/v2/props.d.ts +226 -0
- package/types/DateTimeInput/v2/props.d.ts.map +1 -0
- package/types/exports/b.d.ts +2 -0
- package/types/exports/b.d.ts.map +1 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
---
|
|
2
|
+
describes: DateTimeInput
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
A DateTimeInput component is used to enter a date-time value. It is built as a composition
|
|
6
|
+
of the [DateInput](DateInput) and [TimeSelect](TimeSelect) components.
|
|
7
|
+
|
|
8
|
+
The properties of DateTimeInput prefaced with _date_ are passed to the underlying [DateInput](DateInput)
|
|
9
|
+
(e.g. _dateLabel_ is DateInput's _label_), while the properties prefaced
|
|
10
|
+
with _time_ are forwarded to the underlying
|
|
11
|
+
[TimeSelect](TimeSelect). Others are either shared by both sub-components (e.g. locale), or
|
|
12
|
+
are unique to DateTimeInput (e.g. description).
|
|
13
|
+
|
|
14
|
+
### Localization
|
|
15
|
+
|
|
16
|
+
The component is localized via its `locale` and `timezone` parameters. Both are read from `props`, `context` and from the browser's locale in this priority order. `locale` determines the language and format dates and time are displayed in (e.g. month names, AM/PM or 24-hour format) and the beginning of the week (e.g. Monday in Germany, Sunday in the U.S.) in the dropdown calendar.
|
|
17
|
+
|
|
18
|
+
### Examples
|
|
19
|
+
|
|
20
|
+
#### A DateTimeInput with `columns` layout and a default value:
|
|
21
|
+
|
|
22
|
+
```js
|
|
23
|
+
---
|
|
24
|
+
type: example
|
|
25
|
+
---
|
|
26
|
+
const Example = () => {
|
|
27
|
+
return (
|
|
28
|
+
<div style={{ height: '15rem' }}>
|
|
29
|
+
<DateTimeInput
|
|
30
|
+
description="Pick a date and time"
|
|
31
|
+
datePlaceholder="Choose a date"
|
|
32
|
+
dateRenderLabel="Date"
|
|
33
|
+
timeRenderLabel="Time"
|
|
34
|
+
invalidDateTimeMessage="Invalid date!"
|
|
35
|
+
screenReaderLabels={{ calendarIcon: 'Open calendar', prevMonthButton: 'Previous month', nextMonthButton: 'Next month' }}
|
|
36
|
+
defaultValue="2018-01-18T13:30"
|
|
37
|
+
layout="columns"
|
|
38
|
+
/>
|
|
39
|
+
</div>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
render(<Example />)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
#### A required DateInput with `stacked` layout that warns if the value in the past:
|
|
47
|
+
|
|
48
|
+
This sample code also allows the user to enter an arbitrary time value by setting `allowNonStepInput` to `true`.
|
|
49
|
+
|
|
50
|
+
```js
|
|
51
|
+
---
|
|
52
|
+
type: example
|
|
53
|
+
---
|
|
54
|
+
const Example = () => {
|
|
55
|
+
const [value, setValue] = useState(undefined)
|
|
56
|
+
const [messages, setMessages] = useState([])
|
|
57
|
+
|
|
58
|
+
const onChange = (e, isoDate) => {
|
|
59
|
+
let newMessages = []
|
|
60
|
+
if (!isoDate) {
|
|
61
|
+
// this happens if an invalid date is entered
|
|
62
|
+
setValue(undefined)
|
|
63
|
+
setMessages(newMessages)
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
const now = new Date()
|
|
67
|
+
const newValue = new Date(isoDate)
|
|
68
|
+
if (newValue.valueOf() <= now.valueOf()) {
|
|
69
|
+
newMessages = [{ text: 'That date-time is in the past', type: 'hint' }]
|
|
70
|
+
}
|
|
71
|
+
setValue(isoDate)
|
|
72
|
+
setMessages(newMessages)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const text = value ? new Date(value).toString() : 'N/A'
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div>
|
|
79
|
+
<div style={{ marginBottom: '1em', fontStyle: 'italic' }}>
|
|
80
|
+
<Text>
|
|
81
|
+
You entered:
|
|
82
|
+
<br />
|
|
83
|
+
{text}
|
|
84
|
+
</Text>
|
|
85
|
+
</div>
|
|
86
|
+
<div style={{ height: '14rem' }}>
|
|
87
|
+
<DateTimeInput
|
|
88
|
+
description={
|
|
89
|
+
<ScreenReaderContent>Pick a date and time</ScreenReaderContent>
|
|
90
|
+
}
|
|
91
|
+
datePlaceholder="Choose"
|
|
92
|
+
dateRenderLabel="Date"
|
|
93
|
+
timeRenderLabel="Time"
|
|
94
|
+
screenReaderLabels={{ calendarIcon: 'Open calendar', prevMonthButton: 'Previous month', nextMonthButton: 'Next month' }}
|
|
95
|
+
onChange={onChange}
|
|
96
|
+
layout="stacked"
|
|
97
|
+
value={value}
|
|
98
|
+
invalidDateTimeMessage="Invalid date!"
|
|
99
|
+
messages={messages}
|
|
100
|
+
allowNonStepInput={true}
|
|
101
|
+
isRequired
|
|
102
|
+
/>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
render(<Example />)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
#### A disabled DateTimeInput:
|
|
112
|
+
|
|
113
|
+
```js
|
|
114
|
+
---
|
|
115
|
+
type: example
|
|
116
|
+
---
|
|
117
|
+
<DateTimeInput
|
|
118
|
+
description="Pick a date and time"
|
|
119
|
+
datePlaceholder="Choose a date"
|
|
120
|
+
dateRenderLabel="Date"
|
|
121
|
+
timeRenderLabel="Time"
|
|
122
|
+
screenReaderLabels={{ calendarIcon: 'Open calendar', prevMonthButton: 'Previous month', nextMonthButton: 'Next month' }}
|
|
123
|
+
invalidDateTimeMessage={(dvalue) => { return `'${dvalue} is not valid.` }}
|
|
124
|
+
layout="columns"
|
|
125
|
+
defaultValue="2018-01-18T13:30"
|
|
126
|
+
interaction="disabled"
|
|
127
|
+
/>
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
#### A DateTimeInput in a different locale and timezone where these are set from the React `context`:
|
|
131
|
+
|
|
132
|
+
```js
|
|
133
|
+
---
|
|
134
|
+
type: example
|
|
135
|
+
---
|
|
136
|
+
const Example = () => {
|
|
137
|
+
return (
|
|
138
|
+
<ApplyLocale locale="fr" timezone="Africa/Nairobi">
|
|
139
|
+
<div style={{ height: '14rem' }}>
|
|
140
|
+
<DateTimeInput
|
|
141
|
+
description="Pick a date and time"
|
|
142
|
+
datePlaceholder="Choose a date"
|
|
143
|
+
dateRenderLabel="Date"
|
|
144
|
+
timeRenderLabel="Time"
|
|
145
|
+
screenReaderLabels={{ calendarIcon: 'Open calendar', prevMonthButton: 'Previous month', nextMonthButton: 'Next month' }}
|
|
146
|
+
invalidDateTimeMessage={(dvalue) => {
|
|
147
|
+
return `'${dvalue} is not valid.`
|
|
148
|
+
}}
|
|
149
|
+
layout="columns"
|
|
150
|
+
defaultValue="2018-01-18T16:00"
|
|
151
|
+
/>
|
|
152
|
+
</div>
|
|
153
|
+
</ApplyLocale>
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
render(<Example />)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
#### A `DateTimeInput` with some disabled dates that are supplied via a `string` array:
|
|
161
|
+
|
|
162
|
+
```js
|
|
163
|
+
---
|
|
164
|
+
type: example
|
|
165
|
+
---
|
|
166
|
+
<DateTimeInput
|
|
167
|
+
description="Pick a date and time"
|
|
168
|
+
datePlaceholder="Choose a date"
|
|
169
|
+
dateRenderLabel="Date"
|
|
170
|
+
timeRenderLabel="Time"
|
|
171
|
+
invalidDateTimeMessage="Invalid date"
|
|
172
|
+
disabledDateTimeMessage="Disabled date"
|
|
173
|
+
screenReaderLabels={{ calendarIcon: 'Open calendar', prevMonthButton: 'Previous month', nextMonthButton: 'Next month' }}
|
|
174
|
+
defaultValue="2022-04-08T13:30"
|
|
175
|
+
layout="columns"
|
|
176
|
+
disabledDates={['2022-04-01T13:30', '2022-04-03T13:30', '2022-04-04T13:30']}
|
|
177
|
+
locale="en-us"
|
|
178
|
+
timezone="America/Denver"
|
|
179
|
+
/>
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
#### A `DateTimeInput` with some disabled dates that are supplied via a `function`:
|
|
183
|
+
|
|
184
|
+
```js
|
|
185
|
+
---
|
|
186
|
+
type: example
|
|
187
|
+
---
|
|
188
|
+
const Example = () => {
|
|
189
|
+
const locale = 'en-us'
|
|
190
|
+
const timezone = 'America/Denver'
|
|
191
|
+
|
|
192
|
+
const getDisabledDates = (isoDateToCheck) => {
|
|
193
|
+
const parsed = moment.tz(
|
|
194
|
+
isoDateToCheck,
|
|
195
|
+
[moment.ISO_8601],
|
|
196
|
+
locale,
|
|
197
|
+
timezone
|
|
198
|
+
)
|
|
199
|
+
const now = moment().locale(locale).tz(timezone)
|
|
200
|
+
return parsed.isBefore(now)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return (
|
|
204
|
+
<div style={{ height: '15rem' }}>
|
|
205
|
+
<DateTimeInput
|
|
206
|
+
description="Pick a date and time"
|
|
207
|
+
datePlaceholder="Choose a date"
|
|
208
|
+
dateRenderLabel="Date"
|
|
209
|
+
timeRenderLabel="Time"
|
|
210
|
+
invalidDateTimeMessage={(rawDateValue) =>
|
|
211
|
+
'Invalid date: ' + rawDateValue
|
|
212
|
+
}
|
|
213
|
+
disabledDateTimeMessage={(rawDateValue) =>
|
|
214
|
+
'Disabled date: ' + rawDateValue
|
|
215
|
+
}
|
|
216
|
+
screenReaderLabels={{ calendarIcon: 'Open calendar', prevMonthButton: 'Previous month', nextMonthButton: 'Next month' }}
|
|
217
|
+
defaultValue="2022-04-08T13:30"
|
|
218
|
+
layout="columns"
|
|
219
|
+
disabledDates={getDisabledDates}
|
|
220
|
+
locale={locale}
|
|
221
|
+
timezone={timezone}
|
|
222
|
+
/>
|
|
223
|
+
</div>
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
render(<Example />)
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
#### Programatically reset `DateTimeInput`
|
|
231
|
+
|
|
232
|
+
Due to `onChange` not being called on every typing event, and `value` isn't representing the inner value-state of the component, it's not possible to reset the `DateTimeInput` by setting the `value` to `undefined`. Instead, you can use the `reset` function that is passed to the `DateTimeInput` as a prop.
|
|
233
|
+
|
|
234
|
+
```js
|
|
235
|
+
---
|
|
236
|
+
type: example
|
|
237
|
+
---
|
|
238
|
+
const Example = () => {
|
|
239
|
+
const [date, setDate] = useState('')
|
|
240
|
+
const resetFn = useRef()
|
|
241
|
+
|
|
242
|
+
return (
|
|
243
|
+
<div>
|
|
244
|
+
<DateTimeInput
|
|
245
|
+
description="Pick a date and time"
|
|
246
|
+
datePlaceholder="Choose a date"
|
|
247
|
+
dateRenderLabel="Date"
|
|
248
|
+
timeRenderLabel="Time"
|
|
249
|
+
invalidDateTimeMessage="Invalid date!"
|
|
250
|
+
screenReaderLabels={{ calendarIcon: 'Open calendar', prevMonthButton: 'Previous month', nextMonthButton: 'Next month' }}
|
|
251
|
+
value={date}
|
|
252
|
+
onChange={(e, newDate) => setDate(newDate)}
|
|
253
|
+
reset={(reset) => (resetFn.current = reset)}
|
|
254
|
+
/>
|
|
255
|
+
<Button onClick={() => resetFn.current()}>Clear</Button>
|
|
256
|
+
<Text as="p">{date}</Text>
|
|
257
|
+
</div>
|
|
258
|
+
)
|
|
259
|
+
}
|
|
260
|
+
render(<Example />)
|
|
261
|
+
```
|
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The MIT License (MIT)
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) 2018 - 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, SyntheticEvent } from 'react'
|
|
26
|
+
import { Locale, DateTime, ApplyLocaleContext } from '@instructure/ui-i18n'
|
|
27
|
+
import type { Moment } from '@instructure/ui-i18n'
|
|
28
|
+
import { FormFieldGroup } from '@instructure/ui-form-field/latest'
|
|
29
|
+
import type { FormMessage } from '@instructure/ui-form-field/latest'
|
|
30
|
+
|
|
31
|
+
import { DateInput } from '@instructure/ui-date-input/latest'
|
|
32
|
+
import { TimeSelect } from '@instructure/ui-time-select/latest'
|
|
33
|
+
import type { DateTimeInputProps, DateTimeInputState } from './props'
|
|
34
|
+
import { allowedProps } from './props'
|
|
35
|
+
import { error } from '@instructure/console'
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
---
|
|
39
|
+
category: components
|
|
40
|
+
---
|
|
41
|
+
**/
|
|
42
|
+
class DateTimeInput extends Component<DateTimeInputProps, DateTimeInputState> {
|
|
43
|
+
// extra verbose localized date and time
|
|
44
|
+
private static readonly DEFAULT_MESSAGE_FORMAT = 'LLLL'
|
|
45
|
+
static allowedProps = allowedProps
|
|
46
|
+
static defaultProps = {
|
|
47
|
+
layout: 'inline',
|
|
48
|
+
colSpacing: 'medium',
|
|
49
|
+
rowSpacing: 'small',
|
|
50
|
+
timeStep: 30,
|
|
51
|
+
showMessages: true,
|
|
52
|
+
messageFormat: DateTimeInput.DEFAULT_MESSAGE_FORMAT,
|
|
53
|
+
isRequired: false,
|
|
54
|
+
allowNonStepInput: false
|
|
55
|
+
} as const
|
|
56
|
+
|
|
57
|
+
declare context: React.ContextType<typeof ApplyLocaleContext>
|
|
58
|
+
static contextType = ApplyLocaleContext
|
|
59
|
+
|
|
60
|
+
ref: Element | null = null // This is used by Tooltip for positioning
|
|
61
|
+
|
|
62
|
+
handleRef = (el: Element | null) => {
|
|
63
|
+
this.ref = el
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
constructor(props: DateTimeInputProps) {
|
|
67
|
+
super(props)
|
|
68
|
+
// State needs to be calculated because render could be called before
|
|
69
|
+
// componentDidMount()
|
|
70
|
+
this.state = this.recalculateState(props.value || props.defaultValue)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
componentDidMount() {
|
|
74
|
+
// we'll need to recalculate the state because the context value is
|
|
75
|
+
// set at this point (and it might change locale & timezone)
|
|
76
|
+
const initState = this.recalculateState(
|
|
77
|
+
this.props.value || this.props.defaultValue
|
|
78
|
+
)
|
|
79
|
+
this.setState(initState)
|
|
80
|
+
this.props.reset?.(this.reset)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
componentDidUpdate(prevProps: Readonly<DateTimeInputProps>): void {
|
|
84
|
+
const valueChanged =
|
|
85
|
+
prevProps.value !== this.props.value ||
|
|
86
|
+
prevProps.defaultValue !== this.props.defaultValue
|
|
87
|
+
const isUpdated =
|
|
88
|
+
valueChanged ||
|
|
89
|
+
prevProps.locale !== this.props.locale ||
|
|
90
|
+
prevProps.timezone !== this.props.timezone ||
|
|
91
|
+
prevProps.dateFormat !== this.props.dateFormat ||
|
|
92
|
+
prevProps.messageFormat !== this.props.messageFormat ||
|
|
93
|
+
prevProps.invalidDateTimeMessage !== this.props.invalidDateTimeMessage
|
|
94
|
+
|
|
95
|
+
if (isUpdated) {
|
|
96
|
+
this.setState((_prevState: DateTimeInputState) => {
|
|
97
|
+
return {
|
|
98
|
+
...this.recalculateState(this.props.value || this.props.defaultValue)
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
recalculateState(
|
|
105
|
+
dateStr?: string,
|
|
106
|
+
doNotChangeDate = false,
|
|
107
|
+
doNotChangeTime = false
|
|
108
|
+
): DateTimeInputState {
|
|
109
|
+
let errorMsg: FormMessage | undefined
|
|
110
|
+
if (dateStr) {
|
|
111
|
+
const parsed = DateTime.parse(dateStr, this.locale(), this.timezone())
|
|
112
|
+
if (parsed.isValid()) {
|
|
113
|
+
if (doNotChangeTime && this.state.timeSelectValue) {
|
|
114
|
+
// There is a selected time, adjust the parsed date to its value
|
|
115
|
+
const timeParsed = DateTime.parse(
|
|
116
|
+
this.state.timeSelectValue,
|
|
117
|
+
this.locale(),
|
|
118
|
+
this.timezone()
|
|
119
|
+
)
|
|
120
|
+
parsed.hour(timeParsed.hour()).minute(timeParsed.minute())
|
|
121
|
+
}
|
|
122
|
+
if (doNotChangeDate && this.state.iso) {
|
|
123
|
+
parsed
|
|
124
|
+
.date(this.state.iso.date())
|
|
125
|
+
.month(this.state.iso.month())
|
|
126
|
+
.year(this.state.iso.year())
|
|
127
|
+
}
|
|
128
|
+
if (this.props.initialTimeForNewDate && !this.state?.timeSelectValue) {
|
|
129
|
+
const hour = Number(this.props.initialTimeForNewDate.slice(0, 2))
|
|
130
|
+
const minute = Number(this.props.initialTimeForNewDate.slice(3, 5))
|
|
131
|
+
if (isNaN(hour) || isNaN(minute)) {
|
|
132
|
+
error(
|
|
133
|
+
false,
|
|
134
|
+
`[DateTimeInput] initialTimeForNewDate prop is not in the correct format. Please use HH:MM format.`
|
|
135
|
+
)
|
|
136
|
+
} else if (hour < 0 || hour > 23 || minute > 59 || minute < 0) {
|
|
137
|
+
error(
|
|
138
|
+
false,
|
|
139
|
+
`[DateTimeInput] 0 <= hour < 24 and 0 <= minute < 60 for initialTimeForNewDate prop.`
|
|
140
|
+
)
|
|
141
|
+
} else {
|
|
142
|
+
parsed.hour(hour).minute(minute)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const newTimeSelectValue = parsed.toISOString()
|
|
146
|
+
if (this.isDisabledDate(parsed)) {
|
|
147
|
+
let text =
|
|
148
|
+
typeof this.props.disabledDateTimeMessage === 'function'
|
|
149
|
+
? this.props.disabledDateTimeMessage(parsed.toISOString(true))
|
|
150
|
+
: this.props.disabledDateTimeMessage
|
|
151
|
+
if (!text) {
|
|
152
|
+
text =
|
|
153
|
+
typeof this.props.invalidDateTimeMessage === 'function'
|
|
154
|
+
? this.props.invalidDateTimeMessage(parsed.toISOString(true))
|
|
155
|
+
: this.props.invalidDateTimeMessage
|
|
156
|
+
}
|
|
157
|
+
errorMsg = text ? { text, type: 'error' } : undefined
|
|
158
|
+
return {
|
|
159
|
+
iso: parsed.clone(),
|
|
160
|
+
dateInputText: this.formatDateInput(parsed.toDate()),
|
|
161
|
+
message: errorMsg,
|
|
162
|
+
timeSelectValue: newTimeSelectValue
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
iso: parsed.clone(),
|
|
167
|
+
dateInputText: this.formatDateInput(parsed.toDate()),
|
|
168
|
+
message: {
|
|
169
|
+
type: 'success',
|
|
170
|
+
text: parsed.format(this.props.messageFormat)
|
|
171
|
+
},
|
|
172
|
+
timeSelectValue: newTimeSelectValue
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// if there is no date string clear TimeSelect value
|
|
177
|
+
const clearTimeSelect: Partial<DateTimeInputState> = dateStr
|
|
178
|
+
? {}
|
|
179
|
+
: {
|
|
180
|
+
timeSelectValue: '',
|
|
181
|
+
message: undefined
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
iso: undefined,
|
|
185
|
+
dateInputText: dateStr ? dateStr : '',
|
|
186
|
+
...clearTimeSelect
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
reset = () => this.setState(this.recalculateState())
|
|
191
|
+
|
|
192
|
+
locale(): string {
|
|
193
|
+
if (this.props.locale) {
|
|
194
|
+
return this.props.locale
|
|
195
|
+
} else if (this.context && this.context.locale) {
|
|
196
|
+
return this.context.locale
|
|
197
|
+
}
|
|
198
|
+
return Locale.browserLocale()
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
timezone() {
|
|
202
|
+
if (this.props.timezone) {
|
|
203
|
+
return this.props.timezone
|
|
204
|
+
} else if (this.context && this.context.timezone) {
|
|
205
|
+
return this.context.timezone
|
|
206
|
+
}
|
|
207
|
+
return DateTime.browserTimeZone()
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private formatDateInput(date: Date): string {
|
|
211
|
+
const { dateFormat } = this.props
|
|
212
|
+
if (typeof dateFormat !== 'string' && dateFormat?.formatter) {
|
|
213
|
+
return dateFormat.formatter(date)
|
|
214
|
+
}
|
|
215
|
+
return date.toLocaleDateString(
|
|
216
|
+
typeof dateFormat === 'string' ? dateFormat : this.locale(),
|
|
217
|
+
{
|
|
218
|
+
timeZone: this.timezone(),
|
|
219
|
+
calendar: 'gregory',
|
|
220
|
+
numberingSystem: 'latn'
|
|
221
|
+
}
|
|
222
|
+
)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
isDisabledDate(date: Moment) {
|
|
226
|
+
const disabledDates = this.props.disabledDates
|
|
227
|
+
if (!disabledDates) {
|
|
228
|
+
return false
|
|
229
|
+
}
|
|
230
|
+
if (Array.isArray(disabledDates)) {
|
|
231
|
+
for (const aDisabledDate of disabledDates) {
|
|
232
|
+
if (date.isSame(aDisabledDate, 'day')) {
|
|
233
|
+
return true
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return false
|
|
237
|
+
}
|
|
238
|
+
return disabledDates(date.toISOString())
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
handleDateTextChange = (
|
|
242
|
+
_event: SyntheticEvent,
|
|
243
|
+
inputValue: string,
|
|
244
|
+
_utcDateString: string
|
|
245
|
+
) => {
|
|
246
|
+
this.setState({ dateInputText: inputValue })
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
handleDateValidated = (
|
|
250
|
+
event: SyntheticEvent,
|
|
251
|
+
_inputValue: string,
|
|
252
|
+
utcDateString: string
|
|
253
|
+
) => {
|
|
254
|
+
let newState: DateTimeInputState
|
|
255
|
+
if (
|
|
256
|
+
utcDateString &&
|
|
257
|
+
this.state.timeSelectValue &&
|
|
258
|
+
(!this.state.dateInputText || this.state.dateInputText === '')
|
|
259
|
+
) {
|
|
260
|
+
const timeParsed = DateTime.parse(
|
|
261
|
+
this.state.timeSelectValue,
|
|
262
|
+
this.locale(),
|
|
263
|
+
this.timezone()
|
|
264
|
+
)
|
|
265
|
+
const dateParsed = DateTime.parse(
|
|
266
|
+
utcDateString,
|
|
267
|
+
this.locale(),
|
|
268
|
+
this.timezone()
|
|
269
|
+
)
|
|
270
|
+
const dateParsedAdjusted = dateParsed.set({
|
|
271
|
+
hour: timeParsed.hour(),
|
|
272
|
+
minute: timeParsed.minute()
|
|
273
|
+
})
|
|
274
|
+
newState = this.recalculateState(
|
|
275
|
+
dateParsedAdjusted.toISOString(),
|
|
276
|
+
false,
|
|
277
|
+
false
|
|
278
|
+
)
|
|
279
|
+
} else if (!utcDateString) {
|
|
280
|
+
// invalid date — pass raw text so error message is shown
|
|
281
|
+
newState = this.recalculateState(this.state.dateInputText, false, true)
|
|
282
|
+
} else {
|
|
283
|
+
newState = this.recalculateState(utcDateString, false, true)
|
|
284
|
+
}
|
|
285
|
+
this.changeStateIfNeeded(newState, event)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
updateStateBasedOnTimeSelect = (
|
|
289
|
+
event: SyntheticEvent,
|
|
290
|
+
option: { value?: string; inputText: string }
|
|
291
|
+
) => {
|
|
292
|
+
// this.state.iso is undefined if date is invalid or not set.
|
|
293
|
+
// in this case recalculate with the dateInput's text which will result in
|
|
294
|
+
// an empty valid date (if isRequired is false) or an invalid date.
|
|
295
|
+
const newValue = this.state.iso ? option.value : this.state.dateInputText
|
|
296
|
+
const newState = this.recalculateState(newValue, true, false)
|
|
297
|
+
this.changeStateIfNeeded(newState, event)
|
|
298
|
+
this.setState({ timeSelectValue: option.value })
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
changeStateIfNeeded = (newState: DateTimeInputState, e: SyntheticEvent) => {
|
|
302
|
+
const dateStr = newState.dateInputText
|
|
303
|
+
if (
|
|
304
|
+
(this.props.isRequired && !newState.iso) ||
|
|
305
|
+
(dateStr && dateStr.length > 0 && !newState.iso)
|
|
306
|
+
) {
|
|
307
|
+
const text =
|
|
308
|
+
typeof this.props.invalidDateTimeMessage === 'function'
|
|
309
|
+
? this.props.invalidDateTimeMessage(dateStr ? dateStr : '')
|
|
310
|
+
: this.props.invalidDateTimeMessage
|
|
311
|
+
// eslint-disable-next-line no-param-reassign
|
|
312
|
+
newState.message = { text: text, type: 'error' }
|
|
313
|
+
}
|
|
314
|
+
if (this.areDifferentDates(this.state.iso, newState.iso)) {
|
|
315
|
+
if (typeof this.props.onChange === 'function') {
|
|
316
|
+
const newDate = newState.iso?.toISOString()
|
|
317
|
+
// Timeout is needed here because users might change value in the
|
|
318
|
+
// onChange event lister, which might not execute properly
|
|
319
|
+
setTimeout(() => {
|
|
320
|
+
this.props.onChange?.(e, newDate)
|
|
321
|
+
}, 0)
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
this.setState(newState)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
areDifferentDates = (d1?: Moment, d2?: Moment) => {
|
|
328
|
+
if (!d1 && !d2) {
|
|
329
|
+
return false
|
|
330
|
+
}
|
|
331
|
+
return !d1 || !d2 || !d1.isSame(d2)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
handleBlur = (e: SyntheticEvent) => {
|
|
335
|
+
// when TABbing from the DateInput to TimeInput or visa-versa, the blur
|
|
336
|
+
// happens on the target before the relatedTarget gets focus.
|
|
337
|
+
// The timeout gives it a moment for that to happen
|
|
338
|
+
if (typeof this.props.onBlur === 'function') {
|
|
339
|
+
setTimeout(() => {
|
|
340
|
+
this.props.onBlur?.(e)
|
|
341
|
+
}, 0)
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
render() {
|
|
346
|
+
const {
|
|
347
|
+
description,
|
|
348
|
+
datePlaceholder,
|
|
349
|
+
timePlaceholder,
|
|
350
|
+
dateRenderLabel,
|
|
351
|
+
dateInputRef,
|
|
352
|
+
timeRenderLabel,
|
|
353
|
+
timeFormat,
|
|
354
|
+
timeStep,
|
|
355
|
+
timeInputRef,
|
|
356
|
+
locale,
|
|
357
|
+
timezone,
|
|
358
|
+
showMessages,
|
|
359
|
+
messages,
|
|
360
|
+
layout,
|
|
361
|
+
rowSpacing,
|
|
362
|
+
colSpacing,
|
|
363
|
+
isRequired,
|
|
364
|
+
interaction,
|
|
365
|
+
allowNonStepInput,
|
|
366
|
+
screenReaderLabels,
|
|
367
|
+
disabledDates,
|
|
368
|
+
withYearPicker
|
|
369
|
+
} = this.props
|
|
370
|
+
|
|
371
|
+
const allMessages = [
|
|
372
|
+
...(showMessages && this.state.message ? [this.state.message] : []),
|
|
373
|
+
...(messages || [])
|
|
374
|
+
]
|
|
375
|
+
|
|
376
|
+
const hasError = allMessages.find(
|
|
377
|
+
(m) => m.type === 'newError' || m.type === 'error'
|
|
378
|
+
)
|
|
379
|
+
// if the component is in error state, create an empty error message to pass down to the subcomponents (DateInput and TimeInput) so they get a red outline and red required asterisk
|
|
380
|
+
const subComponentMessages: FormMessage[] = hasError
|
|
381
|
+
? [{ type: 'error', text: '' }]
|
|
382
|
+
: []
|
|
383
|
+
|
|
384
|
+
return (
|
|
385
|
+
<FormFieldGroup
|
|
386
|
+
description={description}
|
|
387
|
+
layout={layout}
|
|
388
|
+
rowSpacing={rowSpacing}
|
|
389
|
+
colSpacing={colSpacing}
|
|
390
|
+
vAlign="top"
|
|
391
|
+
elementRef={this.handleRef}
|
|
392
|
+
isGroup={false}
|
|
393
|
+
messages={[
|
|
394
|
+
...(showMessages && this.state.message ? [this.state.message] : []),
|
|
395
|
+
...(messages || [])
|
|
396
|
+
]}
|
|
397
|
+
data-cid="DateTimeInput"
|
|
398
|
+
>
|
|
399
|
+
<DateInput
|
|
400
|
+
renderLabel={dateRenderLabel}
|
|
401
|
+
screenReaderLabels={screenReaderLabels}
|
|
402
|
+
withYearPicker={withYearPicker}
|
|
403
|
+
value={this.state.dateInputText}
|
|
404
|
+
onChange={this.handleDateTextChange}
|
|
405
|
+
onRequestValidateDate={this.handleDateValidated}
|
|
406
|
+
onBlur={(e) => this.handleBlur(e)}
|
|
407
|
+
inputRef={dateInputRef}
|
|
408
|
+
placeholder={datePlaceholder}
|
|
409
|
+
isRequired={isRequired}
|
|
410
|
+
messages={subComponentMessages}
|
|
411
|
+
interaction={interaction}
|
|
412
|
+
locale={locale}
|
|
413
|
+
timezone={timezone}
|
|
414
|
+
disabledDates={disabledDates}
|
|
415
|
+
dateFormat={this.props.dateFormat}
|
|
416
|
+
/>
|
|
417
|
+
<TimeSelect
|
|
418
|
+
value={this.state.timeSelectValue}
|
|
419
|
+
onChange={this.updateStateBasedOnTimeSelect}
|
|
420
|
+
placeholder={timePlaceholder}
|
|
421
|
+
onBlur={this.handleBlur}
|
|
422
|
+
renderLabel={timeRenderLabel}
|
|
423
|
+
locale={locale}
|
|
424
|
+
format={timeFormat}
|
|
425
|
+
step={timeStep}
|
|
426
|
+
timezone={timezone}
|
|
427
|
+
inputRef={timeInputRef}
|
|
428
|
+
interaction={interaction}
|
|
429
|
+
allowNonStepInput={allowNonStepInput}
|
|
430
|
+
isRequired={isRequired}
|
|
431
|
+
messages={subComponentMessages}
|
|
432
|
+
/>
|
|
433
|
+
</FormFieldGroup>
|
|
434
|
+
)
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
export default DateTimeInput
|
|
439
|
+
export { DateTimeInput }
|