@instructure/ui-date-input 9.5.2-snapshot-7 → 9.6.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 CHANGED
@@ -3,9 +3,23 @@
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
- ## [9.5.2-snapshot-7](https://github.com/instructure/instructure-ui/compare/v9.5.1...v9.5.2-snapshot-7) (2024-07-31)
6
+ # [9.6.0](https://github.com/instructure/instructure-ui/compare/v9.5.2...v9.6.0) (2024-08-14)
7
7
 
8
- **Note:** Version bump only for package @instructure/ui-date-input
8
+
9
+ ### Features
10
+
11
+ * **ui-calendar,ui-date-input:** improve DateInput2 onChange callback, add date formatting option, extend docs ([4e2c23c](https://github.com/instructure/instructure-ui/commit/4e2c23c3288885e49030f1f471d61b2fed29b54c))
12
+
13
+
14
+
15
+
16
+
17
+ ## [9.5.2](https://github.com/instructure/instructure-ui/compare/v9.5.1...v9.5.2) (2024-08-05)
18
+
19
+
20
+ ### Bug Fixes
21
+
22
+ * **ui-calendar,ui-date-input:** fix year picker for non latin based locales; return iso date string in onRequestValidateDate ([d7df0e8](https://github.com/instructure/instructure-ui/commit/d7df0e8d9fc0656e877bd243d9858dc3e5a47198))
9
23
 
10
24
 
11
25
 
@@ -26,7 +26,6 @@ var _IconCalendarMonthLin, _IconArrowOpenEndSoli, _IconArrowOpenStartSo;
26
26
 
27
27
  /** @jsx jsx */
28
28
  import { useState, useEffect, useContext } from 'react';
29
- import moment from 'moment-timezone';
30
29
  import { Calendar } from '@instructure/ui-calendar';
31
30
  import { IconButton } from '@instructure/ui-buttons';
32
31
  import { IconCalendarMonthLine, IconArrowOpenEndSolid, IconArrowOpenStartSolid } from '@instructure/ui-icons';
@@ -36,11 +35,17 @@ import { passthroughProps } from '@instructure/ui-react-utils';
36
35
  import { ApplyLocaleContext, Locale } from '@instructure/ui-i18n';
37
36
  import { jsx } from '@instructure/emotion';
38
37
  import { propTypes } from './props';
39
- function isValidDate(dateString) {
40
- return !isNaN(new Date(dateString).getTime());
38
+ function parseDate(dateString) {
39
+ const date = new Date(dateString);
40
+ return isNaN(date.getTime()) ? '' : date.toISOString();
41
41
  }
42
- function isValidMomentDate(dateString, locale, timezone) {
43
- return moment.tz(dateString, [moment.ISO_8601, 'llll', 'LLLL', 'lll', 'LLL', 'll', 'LL', 'l', 'L'], locale, true, timezone).isValid();
42
+ function defaultDateFormatter(dateString, locale, timezone) {
43
+ return new Date(dateString).toLocaleDateString(locale, {
44
+ month: 'long',
45
+ year: 'numeric',
46
+ day: 'numeric',
47
+ timeZone: timezone
48
+ });
44
49
  }
45
50
 
46
51
  /**
@@ -66,6 +71,8 @@ const DateInput2 = ({
66
71
  locale,
67
72
  timezone,
68
73
  placeholder,
74
+ formatDate = defaultDateFormatter,
75
+ // margin, TODO enable this prop
69
76
  ...rest
70
77
  }) => {
71
78
  const _useState = useState(''),
@@ -82,39 +89,45 @@ const DateInput2 = ({
82
89
  setShowPopover = _useState6[1];
83
90
  const localeContext = useContext(ApplyLocaleContext);
84
91
  useEffect(() => {
92
+ // when `value` is changed, validation removes the error message if passes
93
+ // but it's NOT adding error message if validation fails for better UX
85
94
  validateInput(true);
86
95
  }, [value]);
87
96
  useEffect(() => {
88
97
  setInputMessages(messages || []);
89
98
  }, [messages]);
90
- const handleInputChange = (e, value) => {
91
- onChange === null || onChange === void 0 ? void 0 : onChange(e, value);
99
+ useEffect(() => {
100
+ setSelectedDate(parseDate(value || ''));
101
+ }, []);
102
+ const handleInputChange = (e, newValue, parsedDate = '') => {
103
+ // blur event formats the input which shouldn't trigger parsing
104
+ if (e.type !== 'blur') {
105
+ setSelectedDate(parseDate(newValue));
106
+ }
107
+ onChange === null || onChange === void 0 ? void 0 : onChange(e, newValue, parsedDate);
92
108
  };
93
109
  const handleDateSelected = (dateString, _momentDate, e) => {
94
- const formattedDate = new Date(dateString).toLocaleDateString(getLocale(), {
95
- month: 'long',
96
- year: 'numeric',
97
- day: 'numeric',
98
- timeZone: getTimezone()
99
- });
100
- handleInputChange(e, formattedDate);
110
+ const formattedDate = formatDate(dateString, getLocale(), getTimezone());
111
+ const parsedDate = parseDate(dateString);
112
+ setSelectedDate(parsedDate);
113
+ handleInputChange(e, formattedDate, parsedDate);
101
114
  setShowPopover(false);
102
- onRequestValidateDate === null || onRequestValidateDate === void 0 ? void 0 : onRequestValidateDate(formattedDate, true);
115
+ onRequestValidateDate === null || onRequestValidateDate === void 0 ? void 0 : onRequestValidateDate(dateString, true);
103
116
  };
117
+
118
+ // onlyRemoveError is used to remove the error msg immediately when the user inputs a valid date (and don't wait for blur event)
104
119
  const validateInput = (onlyRemoveError = false) => {
105
- // TODO `isValidDate` and `isValidMomentDate` basically have the same functionality but the latter is a bit more strict (e.g.: `33` is only valid in `isValidMomentDate`)
106
- // in the future we should get rid of moment but currently Calendar is using it for validation too so we can only remove it simultaneously
107
- // otherwise DateInput could pass invalid dates to Calendar and break it
108
- if (isValidDate(value || '') && isValidMomentDate(value || '', getLocale(), getTimezone()) || value === '') {
109
- setSelectedDate(value || '');
120
+ // don't validate empty input
121
+ if (!value || parseDate(value) || selectedDate) {
110
122
  setInputMessages(messages || []);
111
123
  return true;
112
124
  }
113
- if (!onlyRemoveError && typeof invalidDateErrorMessage === 'string') {
114
- setInputMessages(messages => [{
125
+ // only show error if there is no user provided validation callback
126
+ if (!onlyRemoveError && typeof invalidDateErrorMessage === 'string' && !onRequestValidateDate) {
127
+ setInputMessages([{
115
128
  type: 'error',
116
129
  text: invalidDateErrorMessage
117
- }, ...messages]);
130
+ }]);
118
131
  }
119
132
  return false;
120
133
  };
@@ -137,19 +150,15 @@ const DateInput2 = ({
137
150
  };
138
151
  const handleBlur = e => {
139
152
  const isInputValid = validateInput(false);
140
- if (isInputValid && value) {
141
- const formattedDate = new Date(value).toLocaleDateString(getLocale(), {
142
- month: 'long',
143
- year: 'numeric',
144
- day: 'numeric',
145
- timeZone: getTimezone()
146
- });
147
- handleInputChange(e, formattedDate);
153
+ if (isInputValid && selectedDate) {
154
+ const formattedDate = formatDate(selectedDate, getLocale(), getTimezone());
155
+ handleInputChange(e, formattedDate, selectedDate);
148
156
  }
149
157
  onRequestValidateDate === null || onRequestValidateDate === void 0 ? void 0 : onRequestValidateDate(value, isInputValid);
150
158
  onBlur === null || onBlur === void 0 ? void 0 : onBlur(e);
151
159
  };
152
160
  return jsx(TextInput, Object.assign({}, passthroughProps(rest), {
161
+ // margin={'large'} TODO add this prop to TextInput
153
162
  renderLabel: renderLabel,
154
163
  onChange: handleInputChange,
155
164
  onBlur: handleBlur,
@@ -182,8 +191,8 @@ const DateInput2 = ({
182
191
  onDateSelected: handleDateSelected,
183
192
  selectedDate: selectedDate,
184
193
  visibleMonth: selectedDate,
185
- locale: locale,
186
- timezone: timezone,
194
+ locale: getLocale(),
195
+ timezone: getTimezone(),
187
196
  role: "listbox",
188
197
  renderNextMonthButton: jsx(IconButton, {
189
198
  size: "small",
@@ -44,6 +44,7 @@ const propTypes = {
44
44
  invalidDateErrorMessage: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
45
45
  locale: PropTypes.string,
46
46
  timezone: PropTypes.string,
47
- withYearPicker: PropTypes.object
47
+ withYearPicker: PropTypes.object,
48
+ formatDate: PropTypes.func
48
49
  };
49
50
  export { propTypes };
@@ -7,7 +7,6 @@ Object.defineProperty(exports, "__esModule", {
7
7
  exports.default = exports.DateInput2 = void 0;
8
8
  var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
9
9
  var _react = require("react");
10
- var _momentTimezone = _interopRequireDefault(require("moment-timezone"));
11
10
  var _Calendar = require("@instructure/ui-calendar/lib/Calendar");
12
11
  var _IconButton = require("@instructure/ui-buttons/lib/IconButton");
13
12
  var _IconCalendarMonthLine = require("@instructure/ui-icons/lib/IconCalendarMonthLine.js");
@@ -45,11 +44,17 @@ var _IconCalendarMonthLin, _IconArrowOpenEndSoli, _IconArrowOpenStartSo;
45
44
  * SOFTWARE.
46
45
  */
47
46
  /** @jsx jsx */
48
- function isValidDate(dateString) {
49
- return !isNaN(new Date(dateString).getTime());
47
+ function parseDate(dateString) {
48
+ const date = new Date(dateString);
49
+ return isNaN(date.getTime()) ? '' : date.toISOString();
50
50
  }
51
- function isValidMomentDate(dateString, locale, timezone) {
52
- return _momentTimezone.default.tz(dateString, [_momentTimezone.default.ISO_8601, 'llll', 'LLLL', 'lll', 'LLL', 'll', 'LL', 'l', 'L'], locale, true, timezone).isValid();
51
+ function defaultDateFormatter(dateString, locale, timezone) {
52
+ return new Date(dateString).toLocaleDateString(locale, {
53
+ month: 'long',
54
+ year: 'numeric',
55
+ day: 'numeric',
56
+ timeZone: timezone
57
+ });
53
58
  }
54
59
 
55
60
  /**
@@ -75,6 +80,8 @@ const DateInput2 = ({
75
80
  locale,
76
81
  timezone,
77
82
  placeholder,
83
+ formatDate = defaultDateFormatter,
84
+ // margin, TODO enable this prop
78
85
  ...rest
79
86
  }) => {
80
87
  const _useState = (0, _react.useState)(''),
@@ -91,39 +98,45 @@ const DateInput2 = ({
91
98
  setShowPopover = _useState6[1];
92
99
  const localeContext = (0, _react.useContext)(_ApplyLocaleContext.ApplyLocaleContext);
93
100
  (0, _react.useEffect)(() => {
101
+ // when `value` is changed, validation removes the error message if passes
102
+ // but it's NOT adding error message if validation fails for better UX
94
103
  validateInput(true);
95
104
  }, [value]);
96
105
  (0, _react.useEffect)(() => {
97
106
  setInputMessages(messages || []);
98
107
  }, [messages]);
99
- const handleInputChange = (e, value) => {
100
- onChange === null || onChange === void 0 ? void 0 : onChange(e, value);
108
+ (0, _react.useEffect)(() => {
109
+ setSelectedDate(parseDate(value || ''));
110
+ }, []);
111
+ const handleInputChange = (e, newValue, parsedDate = '') => {
112
+ // blur event formats the input which shouldn't trigger parsing
113
+ if (e.type !== 'blur') {
114
+ setSelectedDate(parseDate(newValue));
115
+ }
116
+ onChange === null || onChange === void 0 ? void 0 : onChange(e, newValue, parsedDate);
101
117
  };
102
118
  const handleDateSelected = (dateString, _momentDate, e) => {
103
- const formattedDate = new Date(dateString).toLocaleDateString(getLocale(), {
104
- month: 'long',
105
- year: 'numeric',
106
- day: 'numeric',
107
- timeZone: getTimezone()
108
- });
109
- handleInputChange(e, formattedDate);
119
+ const formattedDate = formatDate(dateString, getLocale(), getTimezone());
120
+ const parsedDate = parseDate(dateString);
121
+ setSelectedDate(parsedDate);
122
+ handleInputChange(e, formattedDate, parsedDate);
110
123
  setShowPopover(false);
111
- onRequestValidateDate === null || onRequestValidateDate === void 0 ? void 0 : onRequestValidateDate(formattedDate, true);
124
+ onRequestValidateDate === null || onRequestValidateDate === void 0 ? void 0 : onRequestValidateDate(dateString, true);
112
125
  };
126
+
127
+ // onlyRemoveError is used to remove the error msg immediately when the user inputs a valid date (and don't wait for blur event)
113
128
  const validateInput = (onlyRemoveError = false) => {
114
- // TODO `isValidDate` and `isValidMomentDate` basically have the same functionality but the latter is a bit more strict (e.g.: `33` is only valid in `isValidMomentDate`)
115
- // in the future we should get rid of moment but currently Calendar is using it for validation too so we can only remove it simultaneously
116
- // otherwise DateInput could pass invalid dates to Calendar and break it
117
- if (isValidDate(value || '') && isValidMomentDate(value || '', getLocale(), getTimezone()) || value === '') {
118
- setSelectedDate(value || '');
129
+ // don't validate empty input
130
+ if (!value || parseDate(value) || selectedDate) {
119
131
  setInputMessages(messages || []);
120
132
  return true;
121
133
  }
122
- if (!onlyRemoveError && typeof invalidDateErrorMessage === 'string') {
123
- setInputMessages(messages => [{
134
+ // only show error if there is no user provided validation callback
135
+ if (!onlyRemoveError && typeof invalidDateErrorMessage === 'string' && !onRequestValidateDate) {
136
+ setInputMessages([{
124
137
  type: 'error',
125
138
  text: invalidDateErrorMessage
126
- }, ...messages]);
139
+ }]);
127
140
  }
128
141
  return false;
129
142
  };
@@ -146,19 +159,15 @@ const DateInput2 = ({
146
159
  };
147
160
  const handleBlur = e => {
148
161
  const isInputValid = validateInput(false);
149
- if (isInputValid && value) {
150
- const formattedDate = new Date(value).toLocaleDateString(getLocale(), {
151
- month: 'long',
152
- year: 'numeric',
153
- day: 'numeric',
154
- timeZone: getTimezone()
155
- });
156
- handleInputChange(e, formattedDate);
162
+ if (isInputValid && selectedDate) {
163
+ const formattedDate = formatDate(selectedDate, getLocale(), getTimezone());
164
+ handleInputChange(e, formattedDate, selectedDate);
157
165
  }
158
166
  onRequestValidateDate === null || onRequestValidateDate === void 0 ? void 0 : onRequestValidateDate(value, isInputValid);
159
167
  onBlur === null || onBlur === void 0 ? void 0 : onBlur(e);
160
168
  };
161
169
  return (0, _emotion.jsx)(_TextInput.TextInput, Object.assign({}, (0, _passthroughProps.passthroughProps)(rest), {
170
+ // margin={'large'} TODO add this prop to TextInput
162
171
  renderLabel: renderLabel,
163
172
  onChange: handleInputChange,
164
173
  onBlur: handleBlur,
@@ -191,8 +200,8 @@ const DateInput2 = ({
191
200
  onDateSelected: handleDateSelected,
192
201
  selectedDate: selectedDate,
193
202
  visibleMonth: selectedDate,
194
- locale: locale,
195
- timezone: timezone,
203
+ locale: getLocale(),
204
+ timezone: getTimezone(),
196
205
  role: "listbox",
197
206
  renderNextMonthButton: (0, _emotion.jsx)(_IconButton.IconButton, {
198
207
  size: "small",
@@ -51,5 +51,6 @@ const propTypes = exports.propTypes = {
51
51
  invalidDateErrorMessage: _propTypes.default.oneOfType([_propTypes.default.func, _propTypes.default.string]),
52
52
  locale: _propTypes.default.string,
53
53
  timezone: _propTypes.default.string,
54
- withYearPicker: _propTypes.default.object
54
+ withYearPicker: _propTypes.default.object,
55
+ formatDate: _propTypes.default.func
55
56
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@instructure/ui-date-input",
3
- "version": "9.5.2-snapshot-7",
3
+ "version": "9.6.0",
4
4
  "description": "A UI component library made by Instructure Inc.",
5
5
  "author": "Instructure, Inc. Engineering and Product Design",
6
6
  "module": "./es/index.js",
@@ -23,11 +23,11 @@
23
23
  },
24
24
  "license": "MIT",
25
25
  "devDependencies": {
26
- "@instructure/ui-axe-check": "9.5.2-snapshot-7",
27
- "@instructure/ui-babel-preset": "9.5.2-snapshot-7",
28
- "@instructure/ui-buttons": "9.5.2-snapshot-7",
29
- "@instructure/ui-scripts": "9.5.2-snapshot-7",
30
- "@instructure/ui-test-utils": "9.5.2-snapshot-7",
26
+ "@instructure/ui-axe-check": "9.6.0",
27
+ "@instructure/ui-babel-preset": "9.6.0",
28
+ "@instructure/ui-buttons": "9.6.0",
29
+ "@instructure/ui-scripts": "9.6.0",
30
+ "@instructure/ui-test-utils": "9.6.0",
31
31
  "@testing-library/jest-dom": "^6.4.6",
32
32
  "@testing-library/react": "^15.0.7",
33
33
  "@testing-library/user-event": "^14.5.2",
@@ -35,20 +35,20 @@
35
35
  },
36
36
  "dependencies": {
37
37
  "@babel/runtime": "^7.24.5",
38
- "@instructure/emotion": "9.5.2-snapshot-7",
39
- "@instructure/shared-types": "9.5.2-snapshot-7",
40
- "@instructure/ui-calendar": "9.5.2-snapshot-7",
41
- "@instructure/ui-form-field": "9.5.2-snapshot-7",
42
- "@instructure/ui-i18n": "9.5.2-snapshot-7",
43
- "@instructure/ui-icons": "9.5.2-snapshot-7",
44
- "@instructure/ui-popover": "9.5.2-snapshot-7",
45
- "@instructure/ui-position": "9.5.2-snapshot-7",
46
- "@instructure/ui-prop-types": "9.5.2-snapshot-7",
47
- "@instructure/ui-react-utils": "9.5.2-snapshot-7",
48
- "@instructure/ui-selectable": "9.5.2-snapshot-7",
49
- "@instructure/ui-testable": "9.5.2-snapshot-7",
50
- "@instructure/ui-text-input": "9.5.2-snapshot-7",
51
- "@instructure/ui-utils": "9.5.2-snapshot-7",
38
+ "@instructure/emotion": "9.6.0",
39
+ "@instructure/shared-types": "9.6.0",
40
+ "@instructure/ui-calendar": "9.6.0",
41
+ "@instructure/ui-form-field": "9.6.0",
42
+ "@instructure/ui-i18n": "9.6.0",
43
+ "@instructure/ui-icons": "9.6.0",
44
+ "@instructure/ui-popover": "9.6.0",
45
+ "@instructure/ui-position": "9.6.0",
46
+ "@instructure/ui-prop-types": "9.6.0",
47
+ "@instructure/ui-react-utils": "9.6.0",
48
+ "@instructure/ui-selectable": "9.6.0",
49
+ "@instructure/ui-testable": "9.6.0",
50
+ "@instructure/ui-text-input": "9.6.0",
51
+ "@instructure/ui-utils": "9.6.0",
52
52
  "moment-timezone": "^0.5.45",
53
53
  "prop-types": "^15.8.1"
54
54
  },
@@ -113,7 +113,11 @@ This component is an updated version of [`DateInput`](/#DateInput) that's easier
113
113
  render(<Example />)
114
114
  ```
115
115
 
116
- ### With custom validation
116
+ ### Date validation
117
+
118
+ By default `DateInput2` only does date validation if the `invalidDateErrorMessage` prop is provided. This uses the browser's `Date` object to try an parse the user provided date and displays the error message if it fails. Validation is only triggered on the blur event of the input field.
119
+
120
+ If you want to do a more complex validation than the above (e.g. only allow a subset of dates) you can use the `onRequestValidateDate` prop to pass a validation function. This function will run on blur or on selecting the date from the picker. The result of the internal validation will be passed to this function. Then you have to set the error messages accordingly. Check the following example for more details:
117
121
 
118
122
  ```js
119
123
  ---
@@ -163,3 +167,101 @@ const Example = () => {
163
167
 
164
168
  render(<Example />)
165
169
  ```
170
+
171
+ ### Date formatting
172
+
173
+ The display format of the dates can be set via the `formatDate` property. It will be applied if the user clicks on a date in the date picker of after blur event from the input field.
174
+ Something to pay attention to is that the date string passed back in the callback function **is in UTC timezone**.
175
+
176
+ ```js
177
+ ---
178
+ type: example
179
+ ---
180
+ const Example = () => {
181
+ const [value1, setValue1] = useState('')
182
+ const [value2, setValue2] = useState('')
183
+ const [value3, setValue3] = useState('')
184
+
185
+ const shortDateFormatFn = (dateString, locale, timezone) => {
186
+ return new Date(dateString).toLocaleDateString(locale, {
187
+ month: 'numeric',
188
+ year: 'numeric',
189
+ day: 'numeric',
190
+ timeZone: timezone,
191
+ })
192
+ }
193
+
194
+ const isoDateFormatFn = (dateString, locale, timezone) => {
195
+ // this is a simple way to get ISO8601 date in a specific timezone but should not be used in production
196
+ // please use a proper date library instead like date-fns, luxon or dayjs
197
+ const localeDate = new Date(dateString).toLocaleDateString('sv', {
198
+ month: 'numeric',
199
+ year: 'numeric',
200
+ day: 'numeric',
201
+ timeZone: timezone,
202
+ })
203
+
204
+ return localeDate
205
+ }
206
+
207
+ return (
208
+ <div style={{display: 'flex', flexDirection: 'column', gap: '1.5rem'}}>
209
+ <DateInput2
210
+ renderLabel="Default format"
211
+ screenReaderLabels={{
212
+ calendarIcon: 'Calendar',
213
+ nextMonthButton: 'Next month',
214
+ prevMonthButton: 'Previous month'
215
+ }}
216
+ isInline
217
+ width="20rem"
218
+ value={value1}
219
+ onChange={(e, value) => setValue1(value)}
220
+ withYearPicker={{
221
+ screenReaderLabel: 'Year picker',
222
+ startYear: 1900,
223
+ endYear: 2024
224
+ }}
225
+ />
226
+ <DateInput2
227
+ renderLabel="Short format in current locale"
228
+ screenReaderLabels={{
229
+ calendarIcon: 'Calendar',
230
+ nextMonthButton: 'Next month',
231
+ prevMonthButton: 'Previous month'
232
+ }}
233
+ isInline
234
+ width="20rem"
235
+ value={value2}
236
+ onChange={(e, value) => setValue2(value)}
237
+ formatDate={shortDateFormatFn}
238
+ withYearPicker={{
239
+ screenReaderLabel: 'Year picker',
240
+ startYear: 1900,
241
+ endYear: 2024
242
+ }}
243
+ />
244
+ <DateInput2
245
+ renderLabel="ISO8601"
246
+ screenReaderLabels={{
247
+ calendarIcon: 'Calendar',
248
+ nextMonthButton: 'Next month',
249
+ prevMonthButton: 'Previous month'
250
+ }}
251
+ isInline
252
+ width="20rem"
253
+ value={value3}
254
+ onChange={(e, value) => setValue3(value)}
255
+ formatDate={isoDateFormatFn}
256
+ withYearPicker={{
257
+ screenReaderLabel: 'Year picker',
258
+ startYear: 1900,
259
+ endYear: 2024
260
+ }}
261
+ />
262
+ </div>
263
+ )
264
+ }
265
+
266
+ render(<Example />)
267
+ ```