@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.
@@ -25,7 +25,6 @@
25
25
  /** @jsx jsx */
26
26
  import { useState, useEffect, useContext } from 'react'
27
27
  import type { SyntheticEvent } from 'react'
28
- import moment from 'moment-timezone'
29
28
  import { Calendar } from '@instructure/ui-calendar'
30
29
  import { IconButton } from '@instructure/ui-buttons'
31
30
  import {
@@ -43,25 +42,24 @@ import { jsx } from '@instructure/emotion'
43
42
  import { propTypes } from './props'
44
43
  import type { DateInput2Props } from './props'
45
44
  import type { FormMessage } from '@instructure/ui-form-field'
45
+ import type { Moment } from '@instructure/ui-i18n'
46
46
 
47
- function isValidDate(dateString: string): boolean {
48
- return !isNaN(new Date(dateString).getTime())
47
+ function parseDate(dateString: string): string {
48
+ const date = new Date(dateString)
49
+ return isNaN(date.getTime()) ? '' : date.toISOString()
49
50
  }
50
51
 
51
- function isValidMomentDate(
52
+ function defaultDateFormatter(
52
53
  dateString: string,
53
54
  locale: string,
54
55
  timezone: string
55
- ): boolean {
56
- return moment
57
- .tz(
58
- dateString,
59
- [moment.ISO_8601, 'llll', 'LLLL', 'lll', 'LLL', 'll', 'LL', 'l', 'L'],
60
- locale,
61
- true,
62
- timezone
63
- )
64
- .isValid()
56
+ ) {
57
+ return new Date(dateString).toLocaleDateString(locale, {
58
+ month: 'long',
59
+ year: 'numeric',
60
+ day: 'numeric',
61
+ timeZone: timezone
62
+ })
65
63
  }
66
64
 
67
65
  /**
@@ -87,6 +85,8 @@ const DateInput2 = ({
87
85
  locale,
88
86
  timezone,
89
87
  placeholder,
88
+ formatDate = defaultDateFormatter,
89
+ // margin, TODO enable this prop
90
90
  ...rest
91
91
  }: DateInput2Props) => {
92
92
  const [selectedDate, setSelectedDate] = useState<string>('')
@@ -97,6 +97,8 @@ const DateInput2 = ({
97
97
  const localeContext = useContext(ApplyLocaleContext)
98
98
 
99
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
100
102
  validateInput(true)
101
103
  }, [value])
102
104
 
@@ -104,46 +106,53 @@ const DateInput2 = ({
104
106
  setInputMessages(messages || [])
105
107
  }, [messages])
106
108
 
107
- const handleInputChange = (e: SyntheticEvent, value: string) => {
108
- onChange?.(e, value)
109
+ useEffect(() => {
110
+ setSelectedDate(parseDate(value || ''))
111
+ }, [])
112
+
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)
109
123
  }
110
124
 
111
125
  const handleDateSelected = (
112
126
  dateString: string,
113
- _momentDate: any, // real type is Moment but used `any` to avoid importing the moment lib
127
+ _momentDate: Moment,
114
128
  e: SyntheticEvent
115
129
  ) => {
116
- const formattedDate = new Date(dateString).toLocaleDateString(getLocale(), {
117
- month: 'long',
118
- year: 'numeric',
119
- day: 'numeric',
120
- timeZone: getTimezone()
121
- })
122
- handleInputChange(e, formattedDate)
130
+ const formattedDate = formatDate(dateString, getLocale(), getTimezone())
131
+ const parsedDate = parseDate(dateString)
132
+ setSelectedDate(parsedDate)
133
+ handleInputChange(e, formattedDate, parsedDate)
123
134
  setShowPopover(false)
124
- onRequestValidateDate?.(formattedDate, true)
135
+ onRequestValidateDate?.(dateString, true)
125
136
  }
126
137
 
138
+ // onlyRemoveError is used to remove the error msg immediately when the user inputs a valid date (and don't wait for blur event)
127
139
  const validateInput = (onlyRemoveError = false): boolean => {
128
- // 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`)
129
- // 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
130
- // otherwise DateInput could pass invalid dates to Calendar and break it
131
- if (
132
- (isValidDate(value || '') &&
133
- isValidMomentDate(value || '', getLocale(), getTimezone())) ||
134
- value === ''
135
- ) {
136
- setSelectedDate(value || '')
140
+ // don't validate empty input
141
+ if (!value || parseDate(value) || selectedDate) {
137
142
  setInputMessages(messages || [])
138
143
  return true
139
144
  }
140
- if (!onlyRemoveError && typeof invalidDateErrorMessage === 'string') {
141
- setInputMessages((messages) => [
145
+ // only show error if there is no user provided validation callback
146
+ if (
147
+ !onlyRemoveError &&
148
+ typeof invalidDateErrorMessage === 'string' &&
149
+ !onRequestValidateDate
150
+ ) {
151
+ setInputMessages([
142
152
  {
143
153
  type: 'error',
144
154
  text: invalidDateErrorMessage
145
- },
146
- ...messages
155
+ }
147
156
  ])
148
157
  }
149
158
 
@@ -171,14 +180,9 @@ const DateInput2 = ({
171
180
 
172
181
  const handleBlur = (e: SyntheticEvent) => {
173
182
  const isInputValid = validateInput(false)
174
- if (isInputValid && value) {
175
- const formattedDate = new Date(value).toLocaleDateString(getLocale(), {
176
- month: 'long',
177
- year: 'numeric',
178
- day: 'numeric',
179
- timeZone: getTimezone()
180
- })
181
- handleInputChange(e, formattedDate)
183
+ if (isInputValid && selectedDate) {
184
+ const formattedDate = formatDate(selectedDate, getLocale(), getTimezone())
185
+ handleInputChange(e, formattedDate, selectedDate)
182
186
  }
183
187
  onRequestValidateDate?.(value, isInputValid)
184
188
  onBlur?.(e)
@@ -187,6 +191,7 @@ const DateInput2 = ({
187
191
  return (
188
192
  <TextInput
189
193
  {...passthroughProps(rest)}
194
+ // margin={'large'} TODO add this prop to TextInput
190
195
  renderLabel={renderLabel}
191
196
  onChange={handleInputChange}
192
197
  onBlur={handleBlur}
@@ -225,8 +230,8 @@ const DateInput2 = ({
225
230
  onDateSelected={handleDateSelected}
226
231
  selectedDate={selectedDate}
227
232
  visibleMonth={selectedDate}
228
- locale={locale}
229
- timezone={timezone}
233
+ locale={getLocale()}
234
+ timezone={getTimezone()}
230
235
  role="listbox"
231
236
  renderNextMonthButton={
232
237
  <IconButton
@@ -23,14 +23,18 @@
23
23
  */
24
24
 
25
25
  import PropTypes from 'prop-types'
26
- import type { SyntheticEvent } from 'react'
26
+ import type { SyntheticEvent, InputHTMLAttributes } from 'react'
27
27
 
28
28
  import { controllable } from '@instructure/ui-prop-types'
29
29
  import { FormPropTypes } from '@instructure/ui-form-field'
30
30
  import type { FormMessage } from '@instructure/ui-form-field'
31
- import type { Renderable, PropValidators } from '@instructure/shared-types'
31
+ import type {
32
+ OtherHTMLAttributes,
33
+ Renderable,
34
+ PropValidators
35
+ } from '@instructure/shared-types'
32
36
 
33
- type DateInput2Props = {
37
+ type DateInput2OwnProps = {
34
38
  /**
35
39
  * Specifies the input label.
36
40
  */
@@ -56,7 +60,11 @@ type DateInput2Props = {
56
60
  /**
57
61
  * Callback fired when the input changes.
58
62
  */
59
- onChange?: (event: React.SyntheticEvent, value: string) => void
63
+ onChange?: (
64
+ event: React.SyntheticEvent,
65
+ inputValue: string,
66
+ dateString: string
67
+ ) => void
60
68
  /**
61
69
  * Callback executed when the input fires a blur event.
62
70
  */
@@ -157,9 +165,27 @@ type DateInput2Props = {
157
165
  startYear: number
158
166
  endYear: number
159
167
  }
168
+
169
+ /**
170
+ * Formatting function for how the date should be displayed inside the input field. It will be applied if the user clicks on a date in the date picker of after blur event from the input field.
171
+ */
172
+ formatDate?: (isoDate: string, locale: string, timezone: string) => string
173
+
174
+ /**
175
+ * Valid values are `0`, `none`, `auto`, `xxx-small`, `xx-small`, `x-small`,
176
+ * `small`, `medium`, `large`, `x-large`, `xx-large`. Apply these values via
177
+ * familiar CSS-like shorthand. For example: `margin="small auto large"`.
178
+ */
179
+ // margin?: Spacing TODO enable this prop
160
180
  }
161
181
 
162
- type PropKeys = keyof DateInput2Props
182
+ type PropKeys = keyof DateInput2OwnProps
183
+
184
+ type DateInput2Props = DateInput2OwnProps &
185
+ OtherHTMLAttributes<
186
+ DateInput2OwnProps,
187
+ InputHTMLAttributes<DateInput2OwnProps & Element>
188
+ >
163
189
 
164
190
  const propTypes: PropValidators<PropKeys> = {
165
191
  renderLabel: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired,
@@ -183,7 +209,8 @@ const propTypes: PropValidators<PropKeys> = {
183
209
  ]),
184
210
  locale: PropTypes.string,
185
211
  timezone: PropTypes.string,
186
- withYearPicker: PropTypes.object
212
+ withYearPicker: PropTypes.object,
213
+ formatDate: PropTypes.func
187
214
  }
188
215
 
189
216
  export type { DateInput2Props }
package/src/index.ts CHANGED
@@ -25,3 +25,4 @@
25
25
  export { DateInput } from './DateInput'
26
26
  export { DateInput2 } from './DateInput2'
27
27
  export type { DateInputProps } from './DateInput/props'
28
+ export type { DateInput2Props } from './DateInput2/props'