@instructure/ui-date-time-input 8.36.1-snapshot-3 → 8.36.1-snapshot-7

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 CHANGED
@@ -3,9 +3,12 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
- ## [8.36.1-snapshot-3](https://github.com/instructure/instructure-ui/compare/v8.36.0...v8.36.1-snapshot-3) (2023-04-11)
6
+ ## [8.36.1-snapshot-7](https://github.com/instructure/instructure-ui/compare/v8.36.0...v8.36.1-snapshot-7) (2023-04-12)
7
7
 
8
- **Note:** Version bump only for package @instructure/ui-date-time-input
8
+
9
+ ### Bug Fixes
10
+
11
+ * **ui-date-time-input:** dateTimeInput does not trigger invalidDateTimeMessage on blur ([307a3f4](https://github.com/instructure/instructure-ui/commit/307a3f4e288aefcc8ae01ef4c39e30015d2c021f))
9
12
 
10
13
 
11
14
 
@@ -75,16 +75,13 @@ let DateTimeInput = (_dec = testable(), _dec(_class = (_class2 = class DateTimeI
75
75
  // timeout is needed here because handleDayClick could be called in the same
76
76
  // frame, and it updates calendarSelectedDate which is read in here.
77
77
  window.setTimeout(() => {
78
- // update state based on the DateInput's text value
79
- if (this.state.dateInputTextChanged || event.key === 'Enter') {
78
+ if (event.key === 'Enter') {
79
+ // user pressed enter, use the selected value in the calendar
80
+ this.updateStateBasedOnDateInput(this.state.calendarSelectedDate, event);
81
+ } else {
82
+ // user clicked outside/tabbed away/pressed esc, try to use the value in dateInputText
80
83
  const dateParsed = this.tryParseDate(this.state.dateInputText);
81
84
  this.updateStateBasedOnDateInput(dateParsed, event);
82
- } else {
83
- // user clicked outside or tabbed away or pressed esc, reset text
84
- this.setState({
85
- dateInputText: this.state.iso ? this.state.iso.format(this.dateFormat) : '',
86
- calendarSelectedDate: this.state.iso ? this.state.iso.clone() : void 0
87
- });
88
85
  }
89
86
  this.setState({
90
87
  isShowingCalendar: false,
@@ -93,13 +90,10 @@ let DateTimeInput = (_dec = testable(), _dec(_class = (_class2 = class DateTimeI
93
90
  }, 0);
94
91
  };
95
92
  this.handleTimeChange = (event, option) => {
96
- let newValue;
97
- if (this.state.iso) {
98
- newValue = option.value;
99
- } else {
100
- // if no date is set just return the input text, it will error
101
- newValue = option.inputText;
102
- }
93
+ // this.state.iso is undefined if date is invalid or not set.
94
+ // in this case recalculate with the dateInput's text which will result in
95
+ // an empty valid date (if isRequired is false) or an invalid date.
96
+ const newValue = this.state.iso ? option.value : this.state.dateInputText;
103
97
  const newState = this.recalculateState(newValue, true, false);
104
98
  this.changeStateIfNeeded(newState, event);
105
99
  this.setState({
@@ -107,19 +101,26 @@ let DateTimeInput = (_dec = testable(), _dec(_class = (_class2 = class DateTimeI
107
101
  });
108
102
  };
109
103
  this.changeStateIfNeeded = (newState, e) => {
110
- this.setState({
111
- message: newState.message
112
- });
113
- if (!newState.iso && !this.state.iso) {
114
- return;
104
+ const dateStr = newState.dateInputText;
105
+ if (this.props.isRequired && !newState.iso || dateStr && dateStr.length > 0 && !newState.iso) {
106
+ const text = typeof this.props.invalidDateTimeMessage === 'function' ? this.props.invalidDateTimeMessage(dateStr ? dateStr : '') : this.props.invalidDateTimeMessage;
107
+ // eslint-disable-next-line no-param-reassign
108
+ newState.message = {
109
+ text: text,
110
+ type: 'error'
111
+ };
115
112
  }
116
- if (!this.state.iso || !newState.iso || !this.state.iso.isSame(newState.iso)) {
117
- this.setState(newState);
118
- if (this.props.onChange) {
119
- var _newState$iso;
120
- this.props.onChange(e, (_newState$iso = newState.iso) === null || _newState$iso === void 0 ? void 0 : _newState$iso.toISOString());
121
- }
113
+ this.setState(newState);
114
+ if (this.areDifferentDates(this.state.iso, newState.iso)) {
115
+ var _this$props$onChange, _this$props, _newState$iso;
116
+ (_this$props$onChange = (_this$props = this.props).onChange) === null || _this$props$onChange === void 0 ? void 0 : _this$props$onChange.call(_this$props, e, (_newState$iso = newState.iso) === null || _newState$iso === void 0 ? void 0 : _newState$iso.toISOString());
117
+ }
118
+ };
119
+ this.areDifferentDates = (d1, d2) => {
120
+ if (!d1 && !d2) {
121
+ return false;
122
122
  }
123
+ return !d1 || !d2 || !d1.isSame(d2);
123
124
  };
124
125
  this.handleBlur = e => {
125
126
  // when TABbing from the DateInput to TimeInput or visa-versa, the blur
@@ -127,8 +128,8 @@ let DateTimeInput = (_dec = testable(), _dec(_class = (_class2 = class DateTimeI
127
128
  // The timeout gives it a moment for that to happen
128
129
  if (typeof this.props.onBlur === 'function') {
129
130
  window.setTimeout(() => {
130
- var _this$props$onBlur, _this$props;
131
- (_this$props$onBlur = (_this$props = this.props).onBlur) === null || _this$props$onBlur === void 0 ? void 0 : _this$props$onBlur.call(_this$props, e);
131
+ var _this$props$onBlur, _this$props2;
132
+ (_this$props$onBlur = (_this$props2 = this.props).onBlur) === null || _this$props$onBlur === void 0 ? void 0 : _this$props$onBlur.call(_this$props2, e);
132
133
  }, 0);
133
134
  }
134
135
  };
@@ -151,8 +152,7 @@ let DateTimeInput = (_dec = testable(), _dec(_class = (_class2 = class DateTimeI
151
152
  });
152
153
  this.setState({
153
154
  calendarSelectedDate: toAlter,
154
- renderedDate: toAlter.clone(),
155
- dateInputText: toAlter.format(this.dateFormat)
155
+ renderedDate: toAlter.clone()
156
156
  });
157
157
  };
158
158
  this.handleSelectPrevDay = _event => {
@@ -169,8 +169,7 @@ let DateTimeInput = (_dec = testable(), _dec(_class = (_class2 = class DateTimeI
169
169
  });
170
170
  this.setState({
171
171
  calendarSelectedDate: toAlter,
172
- renderedDate: toAlter.clone(),
173
- dateInputText: toAlter.format(this.dateFormat)
172
+ renderedDate: toAlter.clone()
174
173
  });
175
174
  };
176
175
  this.handleRenderNextMonth = _event => {
@@ -254,19 +253,11 @@ let DateTimeInput = (_dec = testable(), _dec(_class = (_class2 = class DateTimeI
254
253
  renderedDate: parsed.clone()
255
254
  };
256
255
  }
257
- if (dateStr.length > 0 || this.props.isRequired) {
258
- const text = typeof this.props.invalidDateTimeMessage === 'function' ? this.props.invalidDateTimeMessage(dateStr) : this.props.invalidDateTimeMessage;
259
- errorMsg = text ? {
260
- text,
261
- type: 'error'
262
- } : void 0;
263
- }
264
256
  }
265
257
  return {
266
258
  iso: void 0,
267
259
  calendarSelectedDate: void 0,
268
260
  dateInputText: dateStr ? dateStr : '',
269
- message: errorMsg,
270
261
  renderedDate: DateTime.now(this.locale(), this.timezone()),
271
262
  dateInputTextChanged: false
272
263
  };
@@ -414,24 +405,24 @@ let DateTimeInput = (_dec = testable(), _dec(_class = (_class2 = class DateTimeI
414
405
  });
415
406
  }
416
407
  render() {
417
- const _this$props2 = this.props,
418
- description = _this$props2.description,
419
- datePlaceholder = _this$props2.datePlaceholder,
420
- dateRenderLabel = _this$props2.dateRenderLabel,
421
- dateInputRef = _this$props2.dateInputRef,
422
- timeRenderLabel = _this$props2.timeRenderLabel,
423
- timeFormat = _this$props2.timeFormat,
424
- timeStep = _this$props2.timeStep,
425
- timeInputRef = _this$props2.timeInputRef,
426
- locale = _this$props2.locale,
427
- timezone = _this$props2.timezone,
428
- messages = _this$props2.messages,
429
- layout = _this$props2.layout,
430
- rowSpacing = _this$props2.rowSpacing,
431
- colSpacing = _this$props2.colSpacing,
432
- isRequired = _this$props2.isRequired,
433
- interaction = _this$props2.interaction,
434
- renderWeekdayLabels = _this$props2.renderWeekdayLabels;
408
+ const _this$props3 = this.props,
409
+ description = _this$props3.description,
410
+ datePlaceholder = _this$props3.datePlaceholder,
411
+ dateRenderLabel = _this$props3.dateRenderLabel,
412
+ dateInputRef = _this$props3.dateInputRef,
413
+ timeRenderLabel = _this$props3.timeRenderLabel,
414
+ timeFormat = _this$props3.timeFormat,
415
+ timeStep = _this$props3.timeStep,
416
+ timeInputRef = _this$props3.timeInputRef,
417
+ locale = _this$props3.locale,
418
+ timezone = _this$props3.timezone,
419
+ messages = _this$props3.messages,
420
+ layout = _this$props3.layout,
421
+ rowSpacing = _this$props3.rowSpacing,
422
+ colSpacing = _this$props3.colSpacing,
423
+ isRequired = _this$props3.isRequired,
424
+ interaction = _this$props3.interaction,
425
+ renderWeekdayLabels = _this$props3.renderWeekdayLabels;
435
426
  return /*#__PURE__*/React.createElement(FormFieldGroup, {
436
427
  description: description,
437
428
  layout: layout,
@@ -60,16 +60,13 @@ let DateTimeInput = (_dec = (0, _testable.testable)(), _dec(_class = (_class2 =
60
60
  // timeout is needed here because handleDayClick could be called in the same
61
61
  // frame, and it updates calendarSelectedDate which is read in here.
62
62
  window.setTimeout(() => {
63
- // update state based on the DateInput's text value
64
- if (this.state.dateInputTextChanged || event.key === 'Enter') {
63
+ if (event.key === 'Enter') {
64
+ // user pressed enter, use the selected value in the calendar
65
+ this.updateStateBasedOnDateInput(this.state.calendarSelectedDate, event);
66
+ } else {
67
+ // user clicked outside/tabbed away/pressed esc, try to use the value in dateInputText
65
68
  const dateParsed = this.tryParseDate(this.state.dateInputText);
66
69
  this.updateStateBasedOnDateInput(dateParsed, event);
67
- } else {
68
- // user clicked outside or tabbed away or pressed esc, reset text
69
- this.setState({
70
- dateInputText: this.state.iso ? this.state.iso.format(this.dateFormat) : '',
71
- calendarSelectedDate: this.state.iso ? this.state.iso.clone() : void 0
72
- });
73
70
  }
74
71
  this.setState({
75
72
  isShowingCalendar: false,
@@ -78,13 +75,10 @@ let DateTimeInput = (_dec = (0, _testable.testable)(), _dec(_class = (_class2 =
78
75
  }, 0);
79
76
  };
80
77
  this.handleTimeChange = (event, option) => {
81
- let newValue;
82
- if (this.state.iso) {
83
- newValue = option.value;
84
- } else {
85
- // if no date is set just return the input text, it will error
86
- newValue = option.inputText;
87
- }
78
+ // this.state.iso is undefined if date is invalid or not set.
79
+ // in this case recalculate with the dateInput's text which will result in
80
+ // an empty valid date (if isRequired is false) or an invalid date.
81
+ const newValue = this.state.iso ? option.value : this.state.dateInputText;
88
82
  const newState = this.recalculateState(newValue, true, false);
89
83
  this.changeStateIfNeeded(newState, event);
90
84
  this.setState({
@@ -92,19 +86,26 @@ let DateTimeInput = (_dec = (0, _testable.testable)(), _dec(_class = (_class2 =
92
86
  });
93
87
  };
94
88
  this.changeStateIfNeeded = (newState, e) => {
95
- this.setState({
96
- message: newState.message
97
- });
98
- if (!newState.iso && !this.state.iso) {
99
- return;
89
+ const dateStr = newState.dateInputText;
90
+ if (this.props.isRequired && !newState.iso || dateStr && dateStr.length > 0 && !newState.iso) {
91
+ const text = typeof this.props.invalidDateTimeMessage === 'function' ? this.props.invalidDateTimeMessage(dateStr ? dateStr : '') : this.props.invalidDateTimeMessage;
92
+ // eslint-disable-next-line no-param-reassign
93
+ newState.message = {
94
+ text: text,
95
+ type: 'error'
96
+ };
100
97
  }
101
- if (!this.state.iso || !newState.iso || !this.state.iso.isSame(newState.iso)) {
102
- this.setState(newState);
103
- if (this.props.onChange) {
104
- var _newState$iso;
105
- this.props.onChange(e, (_newState$iso = newState.iso) === null || _newState$iso === void 0 ? void 0 : _newState$iso.toISOString());
106
- }
98
+ this.setState(newState);
99
+ if (this.areDifferentDates(this.state.iso, newState.iso)) {
100
+ var _this$props$onChange, _this$props, _newState$iso;
101
+ (_this$props$onChange = (_this$props = this.props).onChange) === null || _this$props$onChange === void 0 ? void 0 : _this$props$onChange.call(_this$props, e, (_newState$iso = newState.iso) === null || _newState$iso === void 0 ? void 0 : _newState$iso.toISOString());
102
+ }
103
+ };
104
+ this.areDifferentDates = (d1, d2) => {
105
+ if (!d1 && !d2) {
106
+ return false;
107
107
  }
108
+ return !d1 || !d2 || !d1.isSame(d2);
108
109
  };
109
110
  this.handleBlur = e => {
110
111
  // when TABbing from the DateInput to TimeInput or visa-versa, the blur
@@ -112,8 +113,8 @@ let DateTimeInput = (_dec = (0, _testable.testable)(), _dec(_class = (_class2 =
112
113
  // The timeout gives it a moment for that to happen
113
114
  if (typeof this.props.onBlur === 'function') {
114
115
  window.setTimeout(() => {
115
- var _this$props$onBlur, _this$props;
116
- (_this$props$onBlur = (_this$props = this.props).onBlur) === null || _this$props$onBlur === void 0 ? void 0 : _this$props$onBlur.call(_this$props, e);
116
+ var _this$props$onBlur, _this$props2;
117
+ (_this$props$onBlur = (_this$props2 = this.props).onBlur) === null || _this$props$onBlur === void 0 ? void 0 : _this$props$onBlur.call(_this$props2, e);
117
118
  }, 0);
118
119
  }
119
120
  };
@@ -136,8 +137,7 @@ let DateTimeInput = (_dec = (0, _testable.testable)(), _dec(_class = (_class2 =
136
137
  });
137
138
  this.setState({
138
139
  calendarSelectedDate: toAlter,
139
- renderedDate: toAlter.clone(),
140
- dateInputText: toAlter.format(this.dateFormat)
140
+ renderedDate: toAlter.clone()
141
141
  });
142
142
  };
143
143
  this.handleSelectPrevDay = _event => {
@@ -154,8 +154,7 @@ let DateTimeInput = (_dec = (0, _testable.testable)(), _dec(_class = (_class2 =
154
154
  });
155
155
  this.setState({
156
156
  calendarSelectedDate: toAlter,
157
- renderedDate: toAlter.clone(),
158
- dateInputText: toAlter.format(this.dateFormat)
157
+ renderedDate: toAlter.clone()
159
158
  });
160
159
  };
161
160
  this.handleRenderNextMonth = _event => {
@@ -239,19 +238,11 @@ let DateTimeInput = (_dec = (0, _testable.testable)(), _dec(_class = (_class2 =
239
238
  renderedDate: parsed.clone()
240
239
  };
241
240
  }
242
- if (dateStr.length > 0 || this.props.isRequired) {
243
- const text = typeof this.props.invalidDateTimeMessage === 'function' ? this.props.invalidDateTimeMessage(dateStr) : this.props.invalidDateTimeMessage;
244
- errorMsg = text ? {
245
- text,
246
- type: 'error'
247
- } : void 0;
248
- }
249
241
  }
250
242
  return {
251
243
  iso: void 0,
252
244
  calendarSelectedDate: void 0,
253
245
  dateInputText: dateStr ? dateStr : '',
254
- message: errorMsg,
255
246
  renderedDate: _DateTime.DateTime.now(this.locale(), this.timezone()),
256
247
  dateInputTextChanged: false
257
248
  };
@@ -399,24 +390,24 @@ let DateTimeInput = (_dec = (0, _testable.testable)(), _dec(_class = (_class2 =
399
390
  });
400
391
  }
401
392
  render() {
402
- const _this$props2 = this.props,
403
- description = _this$props2.description,
404
- datePlaceholder = _this$props2.datePlaceholder,
405
- dateRenderLabel = _this$props2.dateRenderLabel,
406
- dateInputRef = _this$props2.dateInputRef,
407
- timeRenderLabel = _this$props2.timeRenderLabel,
408
- timeFormat = _this$props2.timeFormat,
409
- timeStep = _this$props2.timeStep,
410
- timeInputRef = _this$props2.timeInputRef,
411
- locale = _this$props2.locale,
412
- timezone = _this$props2.timezone,
413
- messages = _this$props2.messages,
414
- layout = _this$props2.layout,
415
- rowSpacing = _this$props2.rowSpacing,
416
- colSpacing = _this$props2.colSpacing,
417
- isRequired = _this$props2.isRequired,
418
- interaction = _this$props2.interaction,
419
- renderWeekdayLabels = _this$props2.renderWeekdayLabels;
393
+ const _this$props3 = this.props,
394
+ description = _this$props3.description,
395
+ datePlaceholder = _this$props3.datePlaceholder,
396
+ dateRenderLabel = _this$props3.dateRenderLabel,
397
+ dateInputRef = _this$props3.dateInputRef,
398
+ timeRenderLabel = _this$props3.timeRenderLabel,
399
+ timeFormat = _this$props3.timeFormat,
400
+ timeStep = _this$props3.timeStep,
401
+ timeInputRef = _this$props3.timeInputRef,
402
+ locale = _this$props3.locale,
403
+ timezone = _this$props3.timezone,
404
+ messages = _this$props3.messages,
405
+ layout = _this$props3.layout,
406
+ rowSpacing = _this$props3.rowSpacing,
407
+ colSpacing = _this$props3.colSpacing,
408
+ isRequired = _this$props3.isRequired,
409
+ interaction = _this$props3.interaction,
410
+ renderWeekdayLabels = _this$props3.renderWeekdayLabels;
420
411
  return /*#__PURE__*/_react.default.createElement(_FormFieldGroup.FormFieldGroup, {
421
412
  description: description,
422
413
  layout: layout,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@instructure/ui-date-time-input",
3
- "version": "8.36.1-snapshot-3",
3
+ "version": "8.36.1-snapshot-7",
4
4
  "description": "A UI component that allows to select date and time.",
5
5
  "author": "Instructure, Inc. Engineering and Product Design",
6
6
  "module": "./es/index.js",
@@ -23,26 +23,26 @@
23
23
  },
24
24
  "license": "MIT",
25
25
  "devDependencies": {
26
- "@instructure/ui-babel-preset": "8.36.1-snapshot-3",
27
- "@instructure/ui-test-locator": "8.36.1-snapshot-3",
28
- "@instructure/ui-test-utils": "8.36.1-snapshot-3"
26
+ "@instructure/ui-babel-preset": "8.36.1-snapshot-7",
27
+ "@instructure/ui-test-locator": "8.36.1-snapshot-7",
28
+ "@instructure/ui-test-utils": "8.36.1-snapshot-7"
29
29
  },
30
30
  "dependencies": {
31
31
  "@babel/runtime": "^7.20.13",
32
- "@instructure/debounce": "8.36.1-snapshot-3",
33
- "@instructure/emotion": "8.36.1-snapshot-3",
34
- "@instructure/ui-a11y-content": "8.36.1-snapshot-3",
35
- "@instructure/ui-buttons": "8.36.1-snapshot-3",
36
- "@instructure/ui-calendar": "8.36.1-snapshot-3",
37
- "@instructure/ui-date-input": "8.36.1-snapshot-3",
38
- "@instructure/ui-form-field": "8.36.1-snapshot-3",
39
- "@instructure/ui-i18n": "8.36.1-snapshot-3",
40
- "@instructure/ui-icons": "8.36.1-snapshot-3",
41
- "@instructure/ui-prop-types": "8.36.1-snapshot-3",
42
- "@instructure/ui-react-utils": "8.36.1-snapshot-3",
43
- "@instructure/ui-testable": "8.36.1-snapshot-3",
44
- "@instructure/ui-time-select": "8.36.1-snapshot-3",
45
- "@instructure/ui-utils": "8.36.1-snapshot-3",
32
+ "@instructure/debounce": "8.36.1-snapshot-7",
33
+ "@instructure/emotion": "8.36.1-snapshot-7",
34
+ "@instructure/ui-a11y-content": "8.36.1-snapshot-7",
35
+ "@instructure/ui-buttons": "8.36.1-snapshot-7",
36
+ "@instructure/ui-calendar": "8.36.1-snapshot-7",
37
+ "@instructure/ui-date-input": "8.36.1-snapshot-7",
38
+ "@instructure/ui-form-field": "8.36.1-snapshot-7",
39
+ "@instructure/ui-i18n": "8.36.1-snapshot-7",
40
+ "@instructure/ui-icons": "8.36.1-snapshot-7",
41
+ "@instructure/ui-prop-types": "8.36.1-snapshot-7",
42
+ "@instructure/ui-react-utils": "8.36.1-snapshot-7",
43
+ "@instructure/ui-testable": "8.36.1-snapshot-7",
44
+ "@instructure/ui-time-select": "8.36.1-snapshot-7",
45
+ "@instructure/ui-utils": "8.36.1-snapshot-7",
46
46
  "prop-types": "^15.8.1"
47
47
  },
48
48
  "peerDependencies": {
@@ -63,9 +63,14 @@ class Example extends React.Component {
63
63
  }
64
64
 
65
65
  onChange = (e, isoDate) => {
66
+ let messages = []
67
+ if (!isoDate) {
68
+ // this happens if an invalid date is entered
69
+ this.setState({ messages })
70
+ return
71
+ }
66
72
  const now = new Date()
67
73
  const newValue = new Date(isoDate)
68
- let messages = []
69
74
  if ( newValue.valueOf() <= now.valueOf()) {
70
75
  messages = [{text: 'That date-time is in the past', type: 'hint'}]
71
76
  }
@@ -169,19 +169,11 @@ class DateTimeInput extends Component<DateTimeInputProps, DateTimeInputState> {
169
169
  renderedDate: parsed.clone()
170
170
  }
171
171
  }
172
- if (dateStr.length > 0 || this.props.isRequired) {
173
- const text =
174
- typeof this.props.invalidDateTimeMessage === 'function'
175
- ? this.props.invalidDateTimeMessage(dateStr)
176
- : this.props.invalidDateTimeMessage
177
- errorMsg = text ? { text, type: 'error' } : undefined
178
- }
179
172
  }
180
173
  return {
181
174
  iso: undefined,
182
175
  calendarSelectedDate: undefined,
183
176
  dateInputText: dateStr ? dateStr : '',
184
- message: errorMsg,
185
177
  renderedDate: DateTime.now(this.locale(), this.timezone()),
186
178
  dateInputTextChanged: false
187
179
  }
@@ -256,30 +248,20 @@ class DateTimeInput extends Component<DateTimeInputProps, DateTimeInputState> {
256
248
  // timeout is needed here because handleDayClick could be called in the same
257
249
  // frame, and it updates calendarSelectedDate which is read in here.
258
250
  window.setTimeout(() => {
259
- // update state based on the DateInput's text value
260
- if (
261
- this.state.dateInputTextChanged ||
262
- (event as React.KeyboardEvent).key === 'Enter'
263
- ) {
251
+ if ((event as React.KeyboardEvent).key === 'Enter') {
252
+ // user pressed enter, use the selected value in the calendar
253
+ this.updateStateBasedOnDateInput(this.state.calendarSelectedDate, event)
254
+ } else {
255
+ // user clicked outside/tabbed away/pressed esc, try to use the value in dateInputText
264
256
  const dateParsed = this.tryParseDate(this.state.dateInputText)
265
257
  this.updateStateBasedOnDateInput(dateParsed, event)
266
- } else {
267
- // user clicked outside or tabbed away or pressed esc, reset text
268
- this.setState({
269
- dateInputText: this.state.iso
270
- ? this.state.iso.format(this.dateFormat)
271
- : '',
272
- calendarSelectedDate: this.state.iso
273
- ? this.state.iso.clone()
274
- : undefined
275
- })
276
258
  }
277
259
  this.setState({ isShowingCalendar: false, dateInputTextChanged: false })
278
260
  }, 0)
279
261
  }
280
262
 
281
263
  updateStateBasedOnDateInput(
282
- dateParsed: Moment | null,
264
+ dateParsed: Moment | null | undefined,
283
265
  event: SyntheticEvent
284
266
  ) {
285
267
  let newState
@@ -316,34 +298,39 @@ class DateTimeInput extends Component<DateTimeInputProps, DateTimeInputState> {
316
298
  event: SyntheticEvent,
317
299
  option: { value?: string; inputText: string }
318
300
  ) => {
319
- let newValue: string | undefined
320
- if (this.state.iso) {
321
- newValue = option.value
322
- } else {
323
- // if no date is set just return the input text, it will error
324
- newValue = option.inputText
325
- }
301
+ // this.state.iso is undefined if date is invalid or not set.
302
+ // in this case recalculate with the dateInput's text which will result in
303
+ // an empty valid date (if isRequired is false) or an invalid date.
304
+ const newValue = this.state.iso ? option.value : this.state.dateInputText
326
305
  const newState = this.recalculateState(newValue, true, false)
327
-
328
306
  this.changeStateIfNeeded(newState, event)
329
307
  this.setState({ timeSelectValue: option.value })
330
308
  }
331
309
 
332
310
  changeStateIfNeeded = (newState: DateTimeInputState, e: SyntheticEvent) => {
333
- this.setState({ message: newState.message })
334
- if (!newState.iso && !this.state.iso) {
335
- return
336
- }
311
+ const dateStr = newState.dateInputText
337
312
  if (
338
- !this.state.iso ||
339
- !newState.iso ||
340
- !this.state.iso.isSame(newState.iso)
313
+ (this.props.isRequired && !newState.iso) ||
314
+ (dateStr && dateStr.length > 0 && !newState.iso)
341
315
  ) {
342
- this.setState(newState)
343
- if (this.props.onChange) {
344
- this.props.onChange(e, newState.iso?.toISOString())
345
- }
316
+ const text =
317
+ typeof this.props.invalidDateTimeMessage === 'function'
318
+ ? this.props.invalidDateTimeMessage(dateStr ? dateStr : '')
319
+ : this.props.invalidDateTimeMessage
320
+ // eslint-disable-next-line no-param-reassign
321
+ newState.message = { text: text, type: 'error' }
322
+ }
323
+ this.setState(newState)
324
+ if (this.areDifferentDates(this.state.iso, newState.iso)) {
325
+ this.props.onChange?.(e, newState.iso?.toISOString())
326
+ }
327
+ }
328
+
329
+ areDifferentDates = (d1?: Moment, d2?: Moment) => {
330
+ if (!d1 && !d2) {
331
+ return false
346
332
  }
333
+ return !d1 || !d2 || !d1.isSame(d2)
347
334
  }
348
335
 
349
336
  handleBlur = (e: SyntheticEvent) => {
@@ -373,8 +360,7 @@ class DateTimeInput extends Component<DateTimeInputProps, DateTimeInputState> {
373
360
  toAlter.add({ days: 1 })
374
361
  this.setState({
375
362
  calendarSelectedDate: toAlter,
376
- renderedDate: toAlter.clone(),
377
- dateInputText: toAlter.format(this.dateFormat)
363
+ renderedDate: toAlter.clone()
378
364
  })
379
365
  }
380
366
 
@@ -390,8 +376,7 @@ class DateTimeInput extends Component<DateTimeInputProps, DateTimeInputState> {
390
376
  toAlter.subtract({ days: 1 })
391
377
  this.setState({
392
378
  calendarSelectedDate: toAlter,
393
- renderedDate: toAlter.clone(),
394
- dateInputText: toAlter.format(this.dateFormat)
379
+ renderedDate: toAlter.clone()
395
380
  })
396
381
  }
397
382
 
@@ -176,7 +176,8 @@ type DateTimeInputProps = {
176
176
  )[]
177
177
  /**
178
178
  * Specifies if the input is required (its passed down to the native DOM
179
- * elements).
179
+ * elements). If its `true` then an empty input will produce an error message
180
+ * (`invalidDateTimeMessage`)
180
181
  */
181
182
  isRequired?: boolean
182
183
  /**
@@ -232,8 +233,8 @@ type DateTimeInputState = {
232
233
  // the date rendered by the opened calendar. Not selected just determines
233
234
  // which month/year to show
234
235
  renderedDate: Moment
235
- // The value currently displayed in the dateTime component.
236
- // Just the time part is visible
236
+ // The value currently displayed in the dateInput component.
237
+ // Just the date part is visible
237
238
  dateInputText: string
238
239
  // The value currently displayed in the timeSelect component as ISO datetime
239
240
  timeSelectValue?: string