@instructure/ui-date-input 9.6.1-snapshot-2 → 9.7.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.
@@ -36,7 +36,6 @@ const propTypes = exports.propTypes = {
36
36
  renderLabel: _propTypes.default.oneOfType([_propTypes.default.node, _propTypes.default.func]).isRequired,
37
37
  screenReaderLabels: _propTypes.default.object.isRequired,
38
38
  value: (0, _controllable.controllable)(_propTypes.default.string),
39
- size: _propTypes.default.oneOf(['small', 'medium', 'large']),
40
39
  placeholder: _propTypes.default.string,
41
40
  onChange: _propTypes.default.func,
42
41
  onBlur: _propTypes.default.func,
@@ -45,10 +44,10 @@ const propTypes = exports.propTypes = {
45
44
  isInline: _propTypes.default.bool,
46
45
  width: _propTypes.default.string,
47
46
  messages: _propTypes.default.arrayOf(_FormPropTypes.FormPropTypes.message),
48
- onRequestValidateDate: _propTypes.default.func,
49
47
  invalidDateErrorMessage: _propTypes.default.oneOfType([_propTypes.default.func, _propTypes.default.string]),
50
48
  locale: _propTypes.default.string,
51
49
  timezone: _propTypes.default.string,
52
50
  withYearPicker: _propTypes.default.object,
53
- formatDate: _propTypes.default.func
51
+ dateFormat: _propTypes.default.oneOfType([_propTypes.default.string, _propTypes.default.object]),
52
+ onRequestValidateDate: _propTypes.default.func
54
53
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@instructure/ui-date-input",
3
- "version": "9.6.1-snapshot-2",
3
+ "version": "9.7.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.6.1-snapshot-2",
27
- "@instructure/ui-babel-preset": "9.6.1-snapshot-2",
28
- "@instructure/ui-buttons": "9.6.1-snapshot-2",
29
- "@instructure/ui-scripts": "9.6.1-snapshot-2",
30
- "@instructure/ui-test-utils": "9.6.1-snapshot-2",
26
+ "@instructure/ui-axe-check": "9.7.0",
27
+ "@instructure/ui-babel-preset": "9.7.0",
28
+ "@instructure/ui-buttons": "9.7.0",
29
+ "@instructure/ui-scripts": "9.7.0",
30
+ "@instructure/ui-test-utils": "9.7.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.6.1-snapshot-2",
39
- "@instructure/shared-types": "9.6.1-snapshot-2",
40
- "@instructure/ui-calendar": "9.6.1-snapshot-2",
41
- "@instructure/ui-form-field": "9.6.1-snapshot-2",
42
- "@instructure/ui-i18n": "9.6.1-snapshot-2",
43
- "@instructure/ui-icons": "9.6.1-snapshot-2",
44
- "@instructure/ui-popover": "9.6.1-snapshot-2",
45
- "@instructure/ui-position": "9.6.1-snapshot-2",
46
- "@instructure/ui-prop-types": "9.6.1-snapshot-2",
47
- "@instructure/ui-react-utils": "9.6.1-snapshot-2",
48
- "@instructure/ui-selectable": "9.6.1-snapshot-2",
49
- "@instructure/ui-testable": "9.6.1-snapshot-2",
50
- "@instructure/ui-text-input": "9.6.1-snapshot-2",
51
- "@instructure/ui-utils": "9.6.1-snapshot-2",
38
+ "@instructure/emotion": "9.7.0",
39
+ "@instructure/shared-types": "9.7.0",
40
+ "@instructure/ui-calendar": "9.7.0",
41
+ "@instructure/ui-form-field": "9.7.0",
42
+ "@instructure/ui-i18n": "9.7.0",
43
+ "@instructure/ui-icons": "9.7.0",
44
+ "@instructure/ui-popover": "9.7.0",
45
+ "@instructure/ui-position": "9.7.0",
46
+ "@instructure/ui-prop-types": "9.7.0",
47
+ "@instructure/ui-react-utils": "9.7.0",
48
+ "@instructure/ui-selectable": "9.7.0",
49
+ "@instructure/ui-testable": "9.7.0",
50
+ "@instructure/ui-text-input": "9.7.0",
51
+ "@instructure/ui-utils": "9.7.0",
52
52
  "moment-timezone": "^0.5.45",
53
53
  "prop-types": "^15.8.1"
54
54
  },
@@ -2,7 +2,7 @@
2
2
  describes: DateInput
3
3
  ---
4
4
 
5
- > **Important:** You can now use are updated version [`DateInput2`](/#DateInput2) which is easier to configure for developers, has a better UX, better accessibility features and a year picker. We recommend using that instead of `DateInput` which will be deprecated in the future.
5
+ > *Note:* you can now try the updated (but still experimental) [`DateInput2`](/#DateInput2) which is easier to configure for developers, has a better UX, better accessibility features and a year picker. We recommend using that instead of `DateInput` which will be deprecated in the future.
6
6
 
7
7
  The `DateInput` component provides a visual interface for inputting date data.
8
8
 
@@ -2,16 +2,13 @@
2
2
  describes: DateInput2
3
3
  ---
4
4
 
5
- `DateInput2` is an experimental upgrade to the existing [`DateInput`](/#DateInput) component, offering easier configuration, better UX, improved accessibility, and a year picker. While it addresses key limitations of `DateInput`, it's still in the experimental phase, with some missing unit tests and potential (though unlikely) API changes.
6
-
7
- `DateInput` will be deprecated in the future, but for now, developers can start using `DateInput2` and provide feedback.
5
+ > *Warning*: `DateInput2` is an **experimental** upgrade to the existing [`DateInput`](/#DateInput) component, offering easier configuration, better UX, improved accessibility, and a year picker. While it addresses key limitations of `DateInput`, it's still in the experimental phase, with some missing unit tests and potential API changes.
8
6
 
9
7
  ### Minimal config
10
8
 
11
9
  - ```js
12
10
  class Example extends React.Component {
13
- initialDate = '2024-09-09T14:00:00.000Z'
14
- state = { dateString: this.initialDate, inputValue: '' }
11
+ state = { inputValue: '', dateString: '' }
15
12
 
16
13
  render() {
17
14
  return (
@@ -23,17 +20,17 @@ describes: DateInput2
23
20
  nextMonthButton: 'Next month',
24
21
  prevMonthButton: 'Previous month'
25
22
  }}
26
- value={this.state.dateString}
23
+ value={this.state.inputValue}
27
24
  width="20rem"
28
- onChange={(e, dateString, inputValue) => {
25
+ onChange={(e, inputValue, dateString) => {
29
26
  this.setState({ dateString, inputValue })
30
27
  }}
31
28
  invalidDateErrorMessage="Invalid date"
32
29
  />
33
30
  <p>
34
- UTC Date String: <code>{this.state.dateString}</code>
35
- <br />
36
31
  Input Value: <code>{this.state.inputValue}</code>
32
+ <br />
33
+ UTC Date String: <code>{this.state.dateString}</code>
37
34
  </p>
38
35
  </div>
39
36
  )
@@ -45,72 +42,160 @@ describes: DateInput2
45
42
 
46
43
  - ```js
47
44
  const Example = () => {
48
- const [dateString, setDateString] = useState('')
49
45
  const [inputValue, setInputValue] = useState('')
46
+ const [dateString, setDateString] = useState('')
50
47
  return (
51
48
  <div>
52
- <DateInput2
53
- renderLabel="Choose a date"
54
- screenReaderLabels={{
55
- calendarIcon: 'Calendar',
56
- nextMonthButton: 'Next month',
57
- prevMonthButton: 'Previous month'
58
- }}
59
- value={dateString}
60
- width="20rem"
61
- onChange={(e, newDateString, newInputValue) => {
62
- setDateString(newDateString)
63
- setInputValue(newInputValue)
64
- }}
65
- invalidDateErrorMessage="Invalid date"
66
- />
67
- <p>
68
- UTC Date String: <code>{dateString}</code>
69
- <br />
70
- Input Value: <code>{inputValue}</code>
71
- </p>
72
- </div>
49
+ <DateInput2
50
+ renderLabel="Choose a date"
51
+ screenReaderLabels={{
52
+ calendarIcon: 'Calendar',
53
+ nextMonthButton: 'Next month',
54
+ prevMonthButton: 'Previous month'
55
+ }}
56
+ value={inputValue}
57
+ width="20rem"
58
+ onChange={(e, inputValue, dateString) => {
59
+ setInputValue(inputValue)
60
+ setDateString(dateString)
61
+ }}
62
+ invalidDateErrorMessage="Invalid date"
63
+ />
64
+ <p>
65
+ Input Value: <code>{inputValue}</code>
66
+ <br />
67
+ UTC Date String: <code>{dateString}</code>
68
+ </p>
69
+ </div>
73
70
  )
74
71
  }
75
72
 
76
73
  render(<Example />)
77
74
  ```
78
75
 
79
- ### Timezones and UTC
76
+ ### Parsing and formatting dates
80
77
 
81
- In the example above you can see that the date is set via the `value` prop and returned from the `onChange` callback. This date is expected to be in UTC timezone. So if a user chooses September 10th 2024 with the timezone 'Europe/Budapest', the `onChange` function will return `2024-09-09T22:00:00.000Z` because Budapest is two hours ahead of UTC.
78
+ When typing in a date manually (instead of using the included picker), the component tries to parse the date as you type it in. By default parsing is based on the user's locale which determines the order of day, month and year (e.g.: a user with US locale will have MONTH/DAY/YEAR order, and someone with GB locale will have DAY/MONTH/YEAR order).
82
79
 
83
- Altought it would be nice to use the date picker without timezones and leave the time out alltogether but unfortunately you cannot decouple time from dates since the timezone determines when a day ends and another starts. In certain cases this changes the month or even the year. This can affect how you store and load dates from your database: if you want to set a saved date and that date is already timezone adjusted, you have to set it to utc with your date library of choice.
80
+ Any of the following separators can be used when typing a date: `,`, `-`, `.`, `/` or a whitespace however on blur the date will be formatted according to the locale and separators will be changed and leading zeros also adjusted.
84
81
 
85
- ### Parsing dates
82
+ If you want different parsing and formatting then the current locale you can use the `dateFormat` prop which accepts either a string with a name of a different locale (so you can use US date format even if the user is France) or a parser and formatter functions.
86
83
 
87
- When typing a date in the input field instead of using the included picker, the component tries to parse the date as you type it in. To prevent premature parsing (e.g. interpreting `2024` as `2024-01-01T00:00:00.000Z`) parsing only turns on after 10 character. Typed in dates are expected to be in [Date Time String Format](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#date_time_string_format). Any other format are implementation dependant and might differ browser by browser.
84
+ The default parser also has a limitation of not working with years before `1000` and after `9999`. These values are invalid by default but not with custom parsers.
85
+
86
+ ```js
87
+ ---
88
+ type: example
89
+ ---
90
+ const Example = () => {
91
+ const [value, setValue] = useState('')
92
+ const [value2, setValue2] = useState('')
93
+ const [value3, setValue3] = useState('')
94
+
95
+ return (
96
+ <div>
97
+ <p>US locale with default format:</p>
98
+ <DateInput2
99
+ renderLabel="Choose a date"
100
+ screenReaderLabels={{
101
+ calendarIcon: 'Calendar',
102
+ nextMonthButton: 'Next month',
103
+ prevMonthButton: 'Previous month'
104
+ }}
105
+ width="20rem"
106
+ value={value}
107
+ locale="en-us"
108
+ onChange={(e, value) => setValue(value)}
109
+ />
110
+ <p>US locale with german date format:</p>
111
+ <DateInput2
112
+ renderLabel="Choose a date"
113
+ screenReaderLabels={{
114
+ calendarIcon: 'Calendar',
115
+ nextMonthButton: 'Next month',
116
+ prevMonthButton: 'Previous month'
117
+ }}
118
+ width="20rem"
119
+ value={value2}
120
+ locale="en-us"
121
+ dateFormat="de-de"
122
+ onChange={(e, value) => setValue2(value)}
123
+ />
124
+ <p>US locale with ISO date format:</p>
125
+ <DateInput2
126
+ renderLabel="Choose a date"
127
+ screenReaderLabels={{
128
+ calendarIcon: 'Calendar',
129
+ nextMonthButton: 'Next month',
130
+ prevMonthButton: 'Previous month'
131
+ }}
132
+ width="20rem"
133
+ value={value3}
134
+ locale="en-us"
135
+ dateFormat={{
136
+ parser: (input) => {
137
+ // split input on '.', whitespace, '/', ',' or '-' using regex: /[.\s/.-]+/
138
+ // the '+' allows splitting on consecutive delimiters
139
+ const [year, month, day] = input.split(/[,.\s/.-]+/)
140
+ const newDate = new Date(year, month-1, day)
141
+ return isNaN(newDate) ? '' : newDate
142
+ },
143
+ formatter: (date) => {
144
+ // vanilla js formatter but you could use a date library instead
145
+ const year = date.getFullYear()
146
+ // month is zero indexed so add 1
147
+ const month = `${date.getMonth() + 1}`.padStart(2, '0')
148
+ const day = `${date.getDate()}`.padStart(2, '0')
149
+ return `${year}-${month}-${day}`
150
+ }
151
+ }}
152
+ onChange={(e, value) => setValue3(value)}
153
+ />
154
+ </div>
155
+ )
156
+ }
157
+
158
+ render(<Example />)
159
+ ```
160
+
161
+ ### Timezones
162
+
163
+ In the examples above you can see that the `onChange` callback also return a UTC date string. This means it is timezone adjusted. If the timezone is not set via the `timezone` prop, it is calculated/assumed from the user's machine. So if a user chooses September 10th 2024 with the timezone 'Europe/Budapest', the `onChange` function will return `2024-09-09T22:00:00.000Z` because Budapest is two hours ahead of UTC (summertime).
88
164
 
89
165
  ### With year picker
90
166
 
91
167
  - ```js
92
168
  class Example extends React.Component {
93
- state = { value: '' }
169
+ state = { inputValue: '', dateString: '' }
94
170
 
95
171
  render() {
96
172
  return (
97
- <DateInput2
98
- renderLabel="Choose a date"
99
- screenReaderLabels={{
100
- calendarIcon: 'Calendar',
101
- nextMonthButton: 'Next month',
102
- prevMonthButton: 'Previous month'
103
- }}
104
- width="20rem"
105
- value={this.state.value}
106
- onChange={(e, value) => this.setState({ value })}
107
- invalidDateErrorMessage="Invalid date"
108
- withYearPicker={{
109
- screenReaderLabel: 'Year picker',
110
- startYear: 1900,
111
- endYear: 2024
112
- }}
113
- />
173
+ <div>
174
+ <DateInput2
175
+ renderLabel="Choose a date"
176
+ screenReaderLabels={{
177
+ calendarIcon: 'Calendar',
178
+ nextMonthButton: 'Next month',
179
+ prevMonthButton: 'Previous month'
180
+ }}
181
+ value={this.state.inputValue}
182
+ width="20rem"
183
+ onChange={(e, inputValue, dateString) => {
184
+ this.setState({ dateString, inputValue })
185
+ }}
186
+ invalidDateErrorMessage="Invalid date"
187
+ withYearPicker={{
188
+ screenReaderLabel: 'Year picker',
189
+ startYear: 1900,
190
+ endYear: 2024
191
+ }}
192
+ />
193
+ <p>
194
+ Input Value: <code>{this.state.inputValue}</code>
195
+ <br />
196
+ UTC Date String: <code>{this.state.dateString}</code>
197
+ </p>
198
+ </div>
114
199
  )
115
200
  }
116
201
  }
@@ -120,26 +205,36 @@ When typing a date in the input field instead of using the included picker, the
120
205
 
121
206
  - ```js
122
207
  const Example = () => {
123
- const [value, setValue] = useState('')
124
-
208
+ const [inputValue, setInputValue] = useState('')
209
+ const [dateString, setDateString] = useState('')
125
210
  return (
126
- <DateInput2
127
- renderLabel="Choose a date"
128
- screenReaderLabels={{
129
- calendarIcon: 'Calendar',
130
- nextMonthButton: 'Next month',
131
- prevMonthButton: 'Previous month'
132
- }}
133
- width="20rem"
134
- value={value}
135
- onChange={(e, value) => setValue(value)}
136
- invalidDateErrorMessage="Invalid date"
137
- withYearPicker={{
138
- screenReaderLabel: 'Year picker',
139
- startYear: 1900,
140
- endYear: 2024
141
- }}
142
- />
211
+ <div>
212
+ <DateInput2
213
+ renderLabel="Choose a date"
214
+ screenReaderLabels={{
215
+ calendarIcon: 'Calendar',
216
+ nextMonthButton: 'Next month',
217
+ prevMonthButton: 'Previous month'
218
+ }}
219
+ value={inputValue}
220
+ width="20rem"
221
+ onChange={(e, inputValue, dateString) => {
222
+ setInputValue(inputValue)
223
+ setDateString(dateString)
224
+ }}
225
+ invalidDateErrorMessage="Invalid date"
226
+ withYearPicker={{
227
+ screenReaderLabel: 'Year picker',
228
+ startYear: 1900,
229
+ endYear: 2024
230
+ }}
231
+ />
232
+ <p>
233
+ Input Value: <code>{inputValue}</code>
234
+ <br />
235
+ UTC Date String: <code>{dateString}</code>
236
+ </p>
237
+ </div>
143
238
  )
144
239
  }
145
240
 
@@ -148,9 +243,9 @@ When typing a date in the input field instead of using the included picker, the
148
243
 
149
244
  ### Date validation
150
245
 
151
- By default `DateInput2` only does date validation if the `invalidDateErrorMessage` prop is provided. This uses the browser's `Date` object to try and parse the user provided date and displays the error message if it fails. Validation is triggered on the blur event of the input field.
246
+ By default `DateInput2` only does date validation if the `invalidDateErrorMessage` prop is provided. Validation is triggered on the blur event of the input field. Invalid dates are determined current locale.
152
247
 
153
- 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 a 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:
248
+ If you want to do more complex validation (e.g. only allow a subset of dates) you can use the `onRequestValidateDate` and `messages` props.
154
249
 
155
250
  ```js
156
251
  ---
@@ -158,18 +253,24 @@ type: example
158
253
  ---
159
254
  const Example = () => {
160
255
  const [value, setValue] = useState('')
256
+ const [dateString, setDateString] = useState('')
161
257
  const [messages, setMessages] = useState([])
162
258
 
163
- const handleDateValidation = (dateString, isValidDate) => {
164
- if (!isValidDate) {
259
+ const handleDateValidation = (e, inputValue, utcIsoDate) => {
260
+ // utcIsoDate will be an empty string if the input cannot be parsed as a date
261
+
262
+ const date = new Date(utcIsoDate)
263
+
264
+ // don't validate empty input
265
+ if (!utcIsoDate && inputValue.length > 0) {
165
266
  setMessages([{
166
267
  type: 'error',
167
268
  text: 'This is not a valid date'
168
269
  }])
169
- } else if (new Date(dateString) < new Date('January 1, 1900')) {
270
+ } else if (date < new Date('1990-01-01')) {
170
271
  setMessages([{
171
272
  type: 'error',
172
- text: 'Use date after January 1, 1900'
273
+ text: 'Select date after January 1, 1990'
173
274
  }])
174
275
  } else {
175
276
  setMessages([])
@@ -178,7 +279,7 @@ const Example = () => {
178
279
 
179
280
  return (
180
281
  <DateInput2
181
- renderLabel="Choose a date after January 1, 1900"
282
+ renderLabel="Choose a date after January 1, 1990"
182
283
  screenReaderLabels={{
183
284
  calendarIcon: 'Calendar',
184
285
  nextMonthButton: 'Next month',
@@ -201,100 +302,6 @@ const Example = () => {
201
302
  render(<Example />)
202
303
  ```
203
304
 
204
- ### Date formatting
205
-
206
- The display format of the date value can be set via the `formatDate` property. It will be applied if the user clicks on a date in the date picker or after the blur event from the input field.
207
- Something to pay attention to is that the date string passed back in the callback function **is in UTC timezone**.
208
-
209
- ```js
210
- ---
211
- type: example
212
- ---
213
- const Example = () => {
214
- const [value1, setValue1] = useState('')
215
- const [value2, setValue2] = useState('')
216
- const [value3, setValue3] = useState('')
217
-
218
- const shortDateFormatFn = (dateString, locale, timezone) => {
219
- return new Date(dateString).toLocaleDateString(locale, {
220
- month: 'numeric',
221
- year: 'numeric',
222
- day: 'numeric',
223
- timeZone: timezone,
224
- })
225
- }
305
+ ### Date format hint
226
306
 
227
- const isoDateFormatFn = (dateString, locale, timezone) => {
228
- // this is a simple way to get ISO8601 date in a specific timezone but should not be used in production
229
- // please use a proper date library instead like date-fns, luxon or dayjs
230
- const localeDate = new Date(dateString).toLocaleDateString('sv', {
231
- month: 'numeric',
232
- year: 'numeric',
233
- day: 'numeric',
234
- timeZone: timezone,
235
- })
236
-
237
- return localeDate
238
- }
239
-
240
- return (
241
- <div style={{display: 'flex', flexDirection: 'column', gap: '1.5rem'}}>
242
- <DateInput2
243
- renderLabel="Default format"
244
- screenReaderLabels={{
245
- calendarIcon: 'Calendar',
246
- nextMonthButton: 'Next month',
247
- prevMonthButton: 'Previous month'
248
- }}
249
- isInline
250
- width="20rem"
251
- value={value1}
252
- onChange={(e, value) => setValue1(value)}
253
- withYearPicker={{
254
- screenReaderLabel: 'Year picker',
255
- startYear: 1900,
256
- endYear: 2024
257
- }}
258
- />
259
- <DateInput2
260
- renderLabel="Short format in current locale"
261
- screenReaderLabels={{
262
- calendarIcon: 'Calendar',
263
- nextMonthButton: 'Next month',
264
- prevMonthButton: 'Previous month'
265
- }}
266
- isInline
267
- width="20rem"
268
- value={value2}
269
- onChange={(e, value) => setValue2(value)}
270
- formatDate={shortDateFormatFn}
271
- withYearPicker={{
272
- screenReaderLabel: 'Year picker',
273
- startYear: 1900,
274
- endYear: 2024
275
- }}
276
- />
277
- <DateInput2
278
- renderLabel="ISO8601"
279
- screenReaderLabels={{
280
- calendarIcon: 'Calendar',
281
- nextMonthButton: 'Next month',
282
- prevMonthButton: 'Previous month'
283
- }}
284
- isInline
285
- width="20rem"
286
- value={value3}
287
- onChange={(e, value) => setValue3(value)}
288
- formatDate={isoDateFormatFn}
289
- withYearPicker={{
290
- screenReaderLabel: 'Year picker',
291
- startYear: 1900,
292
- endYear: 2024
293
- }}
294
- />
295
- </div>
296
- )
297
- }
298
-
299
- render(<Example />)
300
- ```
307
+ If the `placeholder` property is undefined it will display a hint for the date format (like `DD/MM/YYYY`). Usually it is recommended to leave it as it is for a better user experience.