@instructure/ui-date-input 9.6.0 → 9.6.1-snapshot-2

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.
@@ -44,9 +44,34 @@ import type { DateInput2Props } from './props'
44
44
  import type { FormMessage } from '@instructure/ui-form-field'
45
45
  import type { Moment } from '@instructure/ui-i18n'
46
46
 
47
- function parseDate(dateString: string): string {
47
+ /*
48
+ * Tries parsing a date in a given timezone, if it's not possible, returns an empty string
49
+ * If parsing is successful an ISO formatted datetime string is returned in UTC timezone
50
+ */
51
+ function timezoneDateToUtc(dateString: string, timezone: string): string {
52
+ // Don't try to parse short dateString, they are incomplete
53
+ if (dateString.length < 10) {
54
+ return ''
55
+ }
56
+
57
+ // Create a Date object from the input date string
48
58
  const date = new Date(dateString)
49
- return isNaN(date.getTime()) ? '' : date.toISOString()
59
+
60
+ // Check if the date is valid
61
+ if (isNaN(date.getTime())) {
62
+ return ''
63
+ }
64
+
65
+ // snippet from https://stackoverflow.com/a/57842203
66
+ // but it might need to be improved:
67
+ // "This produces incorrect datetimes for several hours surrounding daylight saving times if the
68
+ // computer running the code is in a zone that doesn't obey the same daylight saving shifts as the target zone."
69
+ const utcDate = new Date(date.toLocaleString('en-US', { timeZone: 'UTC' }))
70
+ const tzDate = new Date(date.toLocaleString('en-US', { timeZone: timezone }))
71
+ const offset = utcDate.getTime() - tzDate.getTime()
72
+ const newDate = new Date(date.getTime() + offset)
73
+
74
+ return newDate.toISOString()
50
75
  }
51
76
 
52
77
  function defaultDateFormatter(
@@ -66,6 +91,8 @@ function defaultDateFormatter(
66
91
  ---
67
92
  category: components
68
93
  ---
94
+
95
+ @module experimental
69
96
  **/
70
97
  const DateInput2 = ({
71
98
  renderLabel,
@@ -89,37 +116,48 @@ const DateInput2 = ({
89
116
  // margin, TODO enable this prop
90
117
  ...rest
91
118
  }: DateInput2Props) => {
92
- const [selectedDate, setSelectedDate] = useState<string>('')
119
+ const localeContext = useContext(ApplyLocaleContext)
120
+
121
+ const getLocale = () => {
122
+ if (locale) {
123
+ return locale
124
+ } else if (localeContext.locale) {
125
+ return localeContext.locale
126
+ }
127
+ // default to the system's locale
128
+ return Locale.browserLocale()
129
+ }
130
+
131
+ const getTimezone = () => {
132
+ if (timezone) {
133
+ return timezone
134
+ } else if (localeContext.timezone) {
135
+ return localeContext.timezone
136
+ }
137
+ // default to the system's timezone
138
+ return Intl.DateTimeFormat().resolvedOptions().timeZone
139
+ }
140
+
141
+ const [inputValue, setInputValue] = useState<string>(
142
+ value ? formatDate(value, getLocale(), getTimezone()) : ''
143
+ )
93
144
  const [inputMessages, setInputMessages] = useState<FormMessage[]>(
94
145
  messages || []
95
146
  )
96
147
  const [showPopover, setShowPopover] = useState<boolean>(false)
97
- const localeContext = useContext(ApplyLocaleContext)
98
-
99
- useEffect(() => {
100
- // when `value` is changed, validation removes the error message if passes
101
- // but it's NOT adding error message if validation fails for better UX
102
- validateInput(true)
103
- }, [value])
104
148
 
105
149
  useEffect(() => {
106
150
  setInputMessages(messages || [])
107
151
  }, [messages])
108
152
 
109
153
  useEffect(() => {
110
- setSelectedDate(parseDate(value || ''))
111
- }, [])
154
+ validateInput(true)
155
+ }, [inputValue])
112
156
 
113
- const handleInputChange = (
114
- e: SyntheticEvent,
115
- newValue: string,
116
- parsedDate: string = ''
117
- ) => {
118
- // blur event formats the input which shouldn't trigger parsing
119
- if (e.type !== 'blur') {
120
- setSelectedDate(parseDate(newValue))
121
- }
122
- onChange?.(e, newValue, parsedDate)
157
+ const handleInputChange = (e: SyntheticEvent, newValue: string) => {
158
+ setInputValue(newValue)
159
+ const parsedInput = timezoneDateToUtc(newValue, getTimezone())
160
+ onChange?.(e, parsedInput, newValue)
123
161
  }
124
162
 
125
163
  const handleDateSelected = (
@@ -128,17 +166,16 @@ const DateInput2 = ({
128
166
  e: SyntheticEvent
129
167
  ) => {
130
168
  const formattedDate = formatDate(dateString, getLocale(), getTimezone())
131
- const parsedDate = parseDate(dateString)
132
- setSelectedDate(parsedDate)
133
- handleInputChange(e, formattedDate, parsedDate)
169
+ setInputValue(formattedDate)
134
170
  setShowPopover(false)
171
+ onChange?.(e, dateString, formattedDate)
135
172
  onRequestValidateDate?.(dateString, true)
136
173
  }
137
174
 
138
175
  // onlyRemoveError is used to remove the error msg immediately when the user inputs a valid date (and don't wait for blur event)
139
176
  const validateInput = (onlyRemoveError = false): boolean => {
140
177
  // don't validate empty input
141
- if (!value || parseDate(value) || selectedDate) {
178
+ if (!inputValue || timezoneDateToUtc(inputValue, getTimezone()) || value) {
142
179
  setInputMessages(messages || [])
143
180
  return true
144
181
  }
@@ -159,32 +196,16 @@ const DateInput2 = ({
159
196
  return false
160
197
  }
161
198
 
162
- const getLocale = () => {
163
- if (locale) {
164
- return locale
165
- } else if (localeContext.locale) {
166
- return localeContext.locale
167
- }
168
- return Locale.browserLocale()
169
- }
170
-
171
- const getTimezone = () => {
172
- if (timezone) {
173
- return timezone
174
- } else if (localeContext.timezone) {
175
- return localeContext.timezone
176
- }
177
- // default to the system's timezone
178
- return Intl.DateTimeFormat().resolvedOptions().timeZone
179
- }
180
-
181
199
  const handleBlur = (e: SyntheticEvent) => {
182
- const isInputValid = validateInput(false)
183
- if (isInputValid && selectedDate) {
184
- const formattedDate = formatDate(selectedDate, getLocale(), getTimezone())
185
- handleInputChange(e, formattedDate, selectedDate)
200
+ if (value) {
201
+ const formattedDate = formatDate(value, getLocale(), getTimezone())
202
+ if (formattedDate !== inputValue) {
203
+ setInputValue(formattedDate)
204
+ onChange?.(e, value, formattedDate)
205
+ }
186
206
  }
187
- onRequestValidateDate?.(value, isInputValid)
207
+ validateInput(false)
208
+ onRequestValidateDate?.(value, !!value)
188
209
  onBlur?.(e)
189
210
  }
190
211
 
@@ -196,7 +217,7 @@ const DateInput2 = ({
196
217
  onChange={handleInputChange}
197
218
  onBlur={handleBlur}
198
219
  isRequired={isRequired}
199
- value={value}
220
+ value={inputValue}
200
221
  placeholder={placeholder}
201
222
  width={width}
202
223
  size={size}
@@ -228,8 +249,8 @@ const DateInput2 = ({
228
249
  <Calendar
229
250
  withYearPicker={withYearPicker}
230
251
  onDateSelected={handleDateSelected}
231
- selectedDate={selectedDate}
232
- visibleMonth={selectedDate}
252
+ selectedDate={value}
253
+ visibleMonth={value}
233
254
  locale={getLocale()}
234
255
  timezone={getTimezone()}
235
256
  role="listbox"
@@ -45,9 +45,9 @@ type DateInput2OwnProps = {
45
45
  nextMonthButton: string
46
46
  }
47
47
  /**
48
- * Specifies the input value.
48
+ * Specifies the input value *before* formatting. The `formatDate` will be applied to it before displaying. Should be a valid, parsable date.
49
49
  */
50
- value?: string // TODO: controllable(PropTypes.string)
50
+ value?: string
51
51
  /**
52
52
  * Specifies the input size.
53
53
  */
@@ -62,8 +62,8 @@ type DateInput2OwnProps = {
62
62
  */
63
63
  onChange?: (
64
64
  event: React.SyntheticEvent,
65
- inputValue: string,
66
- dateString: string
65
+ isoDateString: string,
66
+ formattedValue: string
67
67
  ) => void
68
68
  /**
69
69
  * Callback executed when the input fires a blur event.
@@ -99,21 +99,12 @@ type DateInput2OwnProps = {
99
99
  */
100
100
  messages?: FormMessage[]
101
101
  /**
102
- * Callback fired requesting the calendar be shown.
103
- */
104
- onRequestShowCalendar?: (event: SyntheticEvent) => void
105
- /**
106
- * Callback fired requesting the calendar be hidden.
107
- */
108
- onRequestHideCalendar?: (event: SyntheticEvent) => void
109
- /**
110
- * Callback fired when the input is blurred. Feedback should be provided
111
- * to the user when this function is called if the selected date or input
112
- * value is invalid. The component has an internal check whether the date can
113
- * be parsed to a valid date.
102
+ * Callback fired when the input is blurred or a date is selected from the calendar.
103
+ * Feedback should be provided to the user when this function is called if the selected date or input
104
+ * value is invalid. The component has an internal check whether the date can be parsed to a valid date.
114
105
  */
115
106
  onRequestValidateDate?: (
116
- value?: string,
107
+ isoDateString?: string,
117
108
  internalValidationPassed?: boolean
118
109
  ) => void | FormMessage[]
119
110
  /**
@@ -200,8 +191,6 @@ const propTypes: PropValidators<PropKeys> = {
200
191
  isInline: PropTypes.bool,
201
192
  width: PropTypes.string,
202
193
  messages: PropTypes.arrayOf(FormPropTypes.message),
203
- onRequestShowCalendar: PropTypes.func,
204
- onRequestHideCalendar: PropTypes.func,
205
194
  onRequestValidateDate: PropTypes.func,
206
195
  invalidDateErrorMessage: PropTypes.oneOfType([
207
196
  PropTypes.func,