@instructure/ui-date-input 9.6.1-pr-snapshot-1726659472372 → 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.
package/CHANGELOG.md CHANGED
@@ -3,12 +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
- ## [9.6.1-pr-snapshot-1726659472372](https://github.com/instructure/instructure-ui/compare/v9.6.0...v9.6.1-pr-snapshot-1726659472372) (2024-09-18)
6
+ # [9.7.0](https://github.com/instructure/instructure-ui/compare/v9.6.0...v9.7.0) (2024-09-23)
7
7
 
8
8
 
9
9
  ### Features
10
10
 
11
- * **ui-date-input:** improve DateInput2 api, extend docs ([27bc27f](https://github.com/instructure/instructure-ui/commit/27bc27f8a5e18cf3cabee8b5ea18f75629728ab6))
11
+ * **ui-date-input:** improve DateInput2 api, extend docs ([f369604](https://github.com/instructure/instructure-ui/commit/f3696040d59f9baf9b9a27070e6fbc3d458e4495))
12
12
 
13
13
 
14
14
 
@@ -46,7 +46,7 @@ function parseLocaleDate(dateString = '', locale, timeZone) {
46
46
 
47
47
  // Split string on '.', whitespace, '/', ',' or '-' using regex: /[.\s/.-]+/.
48
48
  // The '+' allows splitting on consecutive delimiters.
49
- // `.filter(Boolean)` is needed because some locales have a delimeter at the end (e.g.: "hu-hu")
49
+ // `.filter(Boolean)` is needed because some locales have a delimeter at the end (e.g.: hungarian dates are formatted as `2024. 09. 19.`)
50
50
  const splitDate = dateString.split(/[,.\s/.-]+/).filter(Boolean);
51
51
 
52
52
  // create a locale formatted new date to later extract the order and delimeter information
@@ -66,13 +66,17 @@ function parseLocaleDate(dateString = '', locale, timeZone) {
66
66
  }
67
67
  });
68
68
 
69
- // sensible year limitations
69
+ // sensible limitations
70
70
  if (!year || !month || !day || year < 1000 || year > 9999) return null;
71
71
 
72
72
  // create utc date from year, month (zero indexed) and day
73
73
  const date = new Date(Date.UTC(year, month - 1, day));
74
+ if (date.getMonth() !== month - 1 || date.getDate() !== day) {
75
+ // Check if the Date object adjusts the values. If it does, the input is invalid.
76
+ return null;
77
+ }
74
78
 
75
- // Format date string in the provided timezone
79
+ // Format date string in the provided timezone. The locale here is irrelevant, we only care about how to time is adjusted for the timezone.
76
80
  const parts = new Intl.DateTimeFormat('en-US', {
77
81
  timeZone,
78
82
  year: 'numeric',
@@ -97,9 +101,9 @@ function parseLocaleDate(dateString = '', locale, timeZone) {
97
101
 
98
102
  // Calculate time difference for timezone offset
99
103
  const timeDiff = new Date(dateInTimezone + 'Z').getTime() - date.getTime();
100
- const newTime = new Date(date.getTime() - timeDiff);
104
+ const utcTime = new Date(date.getTime() - timeDiff);
101
105
  // Return the UTC Date corresponding to the time in the specified timezone
102
- return newTime;
106
+ return utcTime;
103
107
  }
104
108
 
105
109
  /**
@@ -158,7 +162,10 @@ const DateInput2 = ({
158
162
  showPopover = _useState4[0],
159
163
  setShowPopover = _useState4[1];
160
164
  useEffect(() => {
161
- setInputMessages(messages || []);
165
+ // don't set input messages if there is an error set already
166
+ if (!inputMessages) {
167
+ setInputMessages(messages || []);
168
+ }
162
169
  }, [messages]);
163
170
  useEffect(() => {
164
171
  const _parseDate = parseDate(value),
@@ -196,6 +203,21 @@ const DateInput2 = ({
196
203
  numberingSystem: 'latn'
197
204
  });
198
205
  };
206
+ const getDateFromatHint = () => {
207
+ const exampleDate = new Date('2024-09-01');
208
+ const formattedDate = formatDate(exampleDate);
209
+
210
+ // Create a regular expression to find the exact match of the number
211
+ const regex = n => {
212
+ return new RegExp(`(?<!\\d)0*${n}(?!\\d)`, 'g');
213
+ };
214
+
215
+ // Replace the matched number with the same number of dashes
216
+ const year = `${exampleDate.getFullYear()}`;
217
+ const month = `${exampleDate.getMonth() + 1}`;
218
+ const day = `${exampleDate.getDate()}`;
219
+ return formattedDate.replace(regex(year), match => 'Y'.repeat(match.length)).replace(regex(month), match => 'M'.repeat(match.length)).replace(regex(day), match => 'D'.repeat(match.length));
220
+ };
199
221
  const handleInputChange = (e, newValue) => {
200
222
  const _parseDate3 = parseDate(newValue),
201
223
  _parseDate4 = _slicedToArray(_parseDate3, 2),
@@ -234,7 +256,7 @@ const DateInput2 = ({
234
256
  onBlur: handleBlur,
235
257
  isRequired: isRequired,
236
258
  value: value,
237
- placeholder: placeholder,
259
+ placeholder: placeholder !== null && placeholder !== void 0 ? placeholder : getDateFromatHint(),
238
260
  width: width,
239
261
  display: isInline ? 'inline-block' : 'block',
240
262
  messages: inputMessages,
@@ -55,7 +55,7 @@ function parseLocaleDate(dateString = '', locale, timeZone) {
55
55
 
56
56
  // Split string on '.', whitespace, '/', ',' or '-' using regex: /[.\s/.-]+/.
57
57
  // The '+' allows splitting on consecutive delimiters.
58
- // `.filter(Boolean)` is needed because some locales have a delimeter at the end (e.g.: "hu-hu")
58
+ // `.filter(Boolean)` is needed because some locales have a delimeter at the end (e.g.: hungarian dates are formatted as `2024. 09. 19.`)
59
59
  const splitDate = dateString.split(/[,.\s/.-]+/).filter(Boolean);
60
60
 
61
61
  // create a locale formatted new date to later extract the order and delimeter information
@@ -75,13 +75,17 @@ function parseLocaleDate(dateString = '', locale, timeZone) {
75
75
  }
76
76
  });
77
77
 
78
- // sensible year limitations
78
+ // sensible limitations
79
79
  if (!year || !month || !day || year < 1000 || year > 9999) return null;
80
80
 
81
81
  // create utc date from year, month (zero indexed) and day
82
82
  const date = new Date(Date.UTC(year, month - 1, day));
83
+ if (date.getMonth() !== month - 1 || date.getDate() !== day) {
84
+ // Check if the Date object adjusts the values. If it does, the input is invalid.
85
+ return null;
86
+ }
83
87
 
84
- // Format date string in the provided timezone
88
+ // Format date string in the provided timezone. The locale here is irrelevant, we only care about how to time is adjusted for the timezone.
85
89
  const parts = new Intl.DateTimeFormat('en-US', {
86
90
  timeZone,
87
91
  year: 'numeric',
@@ -106,9 +110,9 @@ function parseLocaleDate(dateString = '', locale, timeZone) {
106
110
 
107
111
  // Calculate time difference for timezone offset
108
112
  const timeDiff = new Date(dateInTimezone + 'Z').getTime() - date.getTime();
109
- const newTime = new Date(date.getTime() - timeDiff);
113
+ const utcTime = new Date(date.getTime() - timeDiff);
110
114
  // Return the UTC Date corresponding to the time in the specified timezone
111
- return newTime;
115
+ return utcTime;
112
116
  }
113
117
 
114
118
  /**
@@ -167,7 +171,10 @@ const DateInput2 = ({
167
171
  showPopover = _useState4[0],
168
172
  setShowPopover = _useState4[1];
169
173
  (0, _react.useEffect)(() => {
170
- setInputMessages(messages || []);
174
+ // don't set input messages if there is an error set already
175
+ if (!inputMessages) {
176
+ setInputMessages(messages || []);
177
+ }
171
178
  }, [messages]);
172
179
  (0, _react.useEffect)(() => {
173
180
  const _parseDate = parseDate(value),
@@ -205,6 +212,21 @@ const DateInput2 = ({
205
212
  numberingSystem: 'latn'
206
213
  });
207
214
  };
215
+ const getDateFromatHint = () => {
216
+ const exampleDate = new Date('2024-09-01');
217
+ const formattedDate = formatDate(exampleDate);
218
+
219
+ // Create a regular expression to find the exact match of the number
220
+ const regex = n => {
221
+ return new RegExp(`(?<!\\d)0*${n}(?!\\d)`, 'g');
222
+ };
223
+
224
+ // Replace the matched number with the same number of dashes
225
+ const year = `${exampleDate.getFullYear()}`;
226
+ const month = `${exampleDate.getMonth() + 1}`;
227
+ const day = `${exampleDate.getDate()}`;
228
+ return formattedDate.replace(regex(year), match => 'Y'.repeat(match.length)).replace(regex(month), match => 'M'.repeat(match.length)).replace(regex(day), match => 'D'.repeat(match.length));
229
+ };
208
230
  const handleInputChange = (e, newValue) => {
209
231
  const _parseDate3 = parseDate(newValue),
210
232
  _parseDate4 = (0, _slicedToArray2.default)(_parseDate3, 2),
@@ -243,7 +265,7 @@ const DateInput2 = ({
243
265
  onBlur: handleBlur,
244
266
  isRequired: isRequired,
245
267
  value: value,
246
- placeholder: placeholder,
268
+ placeholder: placeholder !== null && placeholder !== void 0 ? placeholder : getDateFromatHint(),
247
269
  width: width,
248
270
  display: isInline ? 'inline-block' : 'block',
249
271
  messages: inputMessages,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@instructure/ui-date-input",
3
- "version": "9.6.1-pr-snapshot-1726659472372",
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-pr-snapshot-1726659472372",
27
- "@instructure/ui-babel-preset": "9.6.1-pr-snapshot-1726659472372",
28
- "@instructure/ui-buttons": "9.6.1-pr-snapshot-1726659472372",
29
- "@instructure/ui-scripts": "9.6.1-pr-snapshot-1726659472372",
30
- "@instructure/ui-test-utils": "9.6.1-pr-snapshot-1726659472372",
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-pr-snapshot-1726659472372",
39
- "@instructure/shared-types": "9.6.1-pr-snapshot-1726659472372",
40
- "@instructure/ui-calendar": "9.6.1-pr-snapshot-1726659472372",
41
- "@instructure/ui-form-field": "9.6.1-pr-snapshot-1726659472372",
42
- "@instructure/ui-i18n": "9.6.1-pr-snapshot-1726659472372",
43
- "@instructure/ui-icons": "9.6.1-pr-snapshot-1726659472372",
44
- "@instructure/ui-popover": "9.6.1-pr-snapshot-1726659472372",
45
- "@instructure/ui-position": "9.6.1-pr-snapshot-1726659472372",
46
- "@instructure/ui-prop-types": "9.6.1-pr-snapshot-1726659472372",
47
- "@instructure/ui-react-utils": "9.6.1-pr-snapshot-1726659472372",
48
- "@instructure/ui-selectable": "9.6.1-pr-snapshot-1726659472372",
49
- "@instructure/ui-testable": "9.6.1-pr-snapshot-1726659472372",
50
- "@instructure/ui-text-input": "9.6.1-pr-snapshot-1726659472372",
51
- "@instructure/ui-utils": "9.6.1-pr-snapshot-1726659472372",
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
  },
@@ -81,7 +81,7 @@ Any of the following separators can be used when typing a date: `,`, `-`, `.`, `
81
81
 
82
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.
83
83
 
84
- The default parser also have a limitation of excluding years before `1000` and after `9999`. These values are invalid by default but not with custom parsers.
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
85
 
86
86
  ```js
87
87
  ---
@@ -90,10 +90,11 @@ type: example
90
90
  const Example = () => {
91
91
  const [value, setValue] = useState('')
92
92
  const [value2, setValue2] = useState('')
93
+ const [value3, setValue3] = useState('')
93
94
 
94
95
  return (
95
96
  <div>
96
- <p>US locale with german date format:</p>
97
+ <p>US locale with default format:</p>
97
98
  <DateInput2
98
99
  renderLabel="Choose a date"
99
100
  screenReaderLabels={{
@@ -104,10 +105,9 @@ const Example = () => {
104
105
  width="20rem"
105
106
  value={value}
106
107
  locale="en-us"
107
- dateFormat="de-de"
108
108
  onChange={(e, value) => setValue(value)}
109
109
  />
110
- <p>US locale with ISO date format:</p>
110
+ <p>US locale with german date format:</p>
111
111
  <DateInput2
112
112
  renderLabel="Choose a date"
113
113
  screenReaderLabels={{
@@ -118,6 +118,20 @@ const Example = () => {
118
118
  width="20rem"
119
119
  value={value2}
120
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"
121
135
  dateFormat={{
122
136
  parser: (input) => {
123
137
  // split input on '.', whitespace, '/', ',' or '-' using regex: /[.\s/.-]+/
@@ -135,7 +149,7 @@ const Example = () => {
135
149
  return `${year}-${month}-${day}`
136
150
  }
137
151
  }}
138
- onChange={(e, value) => setValue2(value)}
152
+ onChange={(e, value) => setValue3(value)}
139
153
  />
140
154
  </div>
141
155
  )
@@ -229,9 +243,9 @@ In the examples above you can see that the `onChange` callback also return a UTC
229
243
 
230
244
  ### Date validation
231
245
 
232
- 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 based on [parsing](/#DateInput2/#Parsing%20and%20formatting%20dates).
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.
233
247
 
234
- If you want to do a more complex validation than the above (e.g. only allow a subset of dates) you can use the `onBlur` and `messages` props.
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.
235
249
 
236
250
  ```js
237
251
  ---
@@ -243,8 +257,10 @@ const Example = () => {
243
257
  const [messages, setMessages] = useState([])
244
258
 
245
259
  const handleDateValidation = (e, inputValue, utcIsoDate) => {
260
+ // utcIsoDate will be an empty string if the input cannot be parsed as a date
261
+
246
262
  const date = new Date(utcIsoDate)
247
- console.log(utcIsoDate)
263
+
248
264
  // don't validate empty input
249
265
  if (!utcIsoDate && inputValue.length > 0) {
250
266
  setMessages([{
@@ -285,3 +301,7 @@ const Example = () => {
285
301
 
286
302
  render(<Example />)
287
303
  ```
304
+
305
+ ### Date format hint
306
+
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.
@@ -55,7 +55,7 @@ function parseLocaleDate(dateString: string = '', locale: string, timeZone: stri
55
55
 
56
56
  // Split string on '.', whitespace, '/', ',' or '-' using regex: /[.\s/.-]+/.
57
57
  // The '+' allows splitting on consecutive delimiters.
58
- // `.filter(Boolean)` is needed because some locales have a delimeter at the end (e.g.: "hu-hu")
58
+ // `.filter(Boolean)` is needed because some locales have a delimeter at the end (e.g.: hungarian dates are formatted as `2024. 09. 19.`)
59
59
  const splitDate = dateString.split(/[,.\s/.-]+/).filter(Boolean)
60
60
 
61
61
  // create a locale formatted new date to later extract the order and delimeter information
@@ -76,13 +76,18 @@ function parseLocaleDate(dateString: string = '', locale: string, timeZone: stri
76
76
  }
77
77
  })
78
78
 
79
- // sensible year limitations
79
+ // sensible limitations
80
80
  if (!year || !month || !day || year < 1000 || year > 9999) return null
81
81
 
82
82
  // create utc date from year, month (zero indexed) and day
83
83
  const date = new Date(Date.UTC(year, month - 1, day))
84
84
 
85
- // Format date string in the provided timezone
85
+ if (date.getMonth() !== month - 1 || date.getDate() !== day) {
86
+ // Check if the Date object adjusts the values. If it does, the input is invalid.
87
+ return null
88
+ }
89
+
90
+ // Format date string in the provided timezone. The locale here is irrelevant, we only care about how to time is adjusted for the timezone.
86
91
  const parts = new Intl.DateTimeFormat('en-US', {
87
92
  timeZone,
88
93
  year: 'numeric',
@@ -109,9 +114,9 @@ function parseLocaleDate(dateString: string = '', locale: string, timeZone: stri
109
114
 
110
115
  // Calculate time difference for timezone offset
111
116
  const timeDiff = new Date(dateInTimezone + 'Z').getTime() - date.getTime()
112
- const newTime = new Date(date.getTime() - timeDiff)
117
+ const utcTime = new Date(date.getTime() - timeDiff)
113
118
  // Return the UTC Date corresponding to the time in the specified timezone
114
- return newTime
119
+ return utcTime
115
120
  }
116
121
 
117
122
  /**
@@ -170,7 +175,10 @@ const DateInput2 = ({
170
175
  const [showPopover, setShowPopover] = useState<boolean>(false)
171
176
 
172
177
  useEffect(() => {
173
- setInputMessages(messages || [])
178
+ // don't set input messages if there is an error set already
179
+ if (!inputMessages) {
180
+ setInputMessages(messages || [])
181
+ }
174
182
  }, [messages])
175
183
 
176
184
  useEffect(() => {
@@ -206,6 +214,25 @@ const DateInput2 = ({
206
214
  return date.toLocaleDateString(typeof dateFormat === 'string' ? dateFormat : getLocale(), {timeZone: getTimezone(), calendar: 'gregory', numberingSystem: 'latn'})
207
215
  }
208
216
 
217
+ const getDateFromatHint = () => {
218
+ const exampleDate = new Date('2024-09-01')
219
+ const formattedDate = formatDate(exampleDate)
220
+
221
+ // Create a regular expression to find the exact match of the number
222
+ const regex = (n: string) => {
223
+ return new RegExp(`(?<!\\d)0*${n}(?!\\d)`, 'g')
224
+ }
225
+
226
+ // Replace the matched number with the same number of dashes
227
+ const year = `${exampleDate.getFullYear()}`
228
+ const month = `${exampleDate.getMonth() + 1}`
229
+ const day = `${exampleDate.getDate()}`
230
+ return formattedDate
231
+ .replace(regex(year), (match) => 'Y'.repeat(match.length))
232
+ .replace(regex(month), (match) => 'M'.repeat(match.length))
233
+ .replace(regex(day), (match) => 'D'.repeat(match.length))
234
+ }
235
+
209
236
  const handleInputChange = (e: SyntheticEvent, newValue: string) => {
210
237
  const [, utcIsoDate] = parseDate(newValue)
211
238
  onChange?.(e, newValue, utcIsoDate)
@@ -251,7 +278,7 @@ const DateInput2 = ({
251
278
  onBlur={handleBlur}
252
279
  isRequired={isRequired}
253
280
  value={value}
254
- placeholder={placeholder}
281
+ placeholder={placeholder ?? getDateFromatHint()}
255
282
  width={width}
256
283
  display={isInline ? 'inline-block' : 'block'}
257
284
  messages={inputMessages}
@@ -45,12 +45,11 @@ type DateInput2OwnProps = {
45
45
  nextMonthButton: string
46
46
  }
47
47
  /**
48
- * Specifies the input value *before* formatting. The `formatDate` will be applied to it before displaying. Should be a valid, parsable date.
48
+ * Specifies the input value.
49
49
  */
50
50
  value?: string
51
51
  /**
52
- * Html placeholder text to display when the input has no value. This should
53
- * be hint text, not a label replacement.
52
+ * Placeholder text for the input field. If it's left undefined it will display a hint for the date format (like `DD/MM/YYYY`).
54
53
  */
55
54
  placeholder?: string
56
55
  /**
@@ -58,7 +57,7 @@ type DateInput2OwnProps = {
58
57
  */
59
58
  onChange?: (
60
59
  event: React.SyntheticEvent,
61
- value: string,
60
+ inputValue: string,
62
61
  utcDateString: string
63
62
  ) => void
64
63
  /**
@@ -121,7 +120,7 @@ type DateInput2OwnProps = {
121
120
  * This property can also be set via a context property and if both are set
122
121
  * then the component property takes precedence over the context property.
123
122
  *
124
- * The web browser's timezone will be used if no value is set via a component
123
+ * The system timezone will be used if no value is set via a component
125
124
  * property or a context property.
126
125
  **/
127
126
  timezone?: string
@@ -147,7 +146,7 @@ type DateInput2OwnProps = {
147
146
  * By default the date format is determined by the locale but can be changed via this prop to an alternate locale (passing it in as a string) or a custom parser and formatter (both as functions)
148
147
  */
149
148
  dateFormat?: {
150
- parser: (input: string) => Date
149
+ parser: (input: string) => Date | null
151
150
  formatter: (date: Date) => string
152
151
  } | string
153
152