@instructure/ui-date-input 8.52.1-snapshot-7 → 8.53.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.
@@ -45,12 +45,15 @@ import {
45
45
  } from '@instructure/ui-react-utils'
46
46
  import { testable } from '@instructure/ui-testable'
47
47
 
48
+ import { DateTime, ApplyLocaleContext, Locale } from '@instructure/ui-i18n'
49
+
48
50
  import { withStyle, jsx } from '@instructure/emotion'
49
51
 
50
52
  import generateStyle from './styles'
51
53
 
52
54
  import { propTypes, allowedProps } from './props'
53
- import type { DateInputProps } from './props'
55
+ import type { DateInputProps, DateInputState } from './props'
56
+ import type { FormMessage } from '@instructure/ui-form-field'
54
57
 
55
58
  /**
56
59
  ---
@@ -61,9 +64,10 @@ See <https://instructure.design/#DateInput/>
61
64
  **/
62
65
  @withStyle(generateStyle, null)
63
66
  @testable()
64
- class DateInput extends Component<DateInputProps> {
67
+ class DateInput extends Component<DateInputProps, DateInputState> {
65
68
  static readonly componentId = 'DateInput'
66
69
  static Day = Calendar.Day
70
+ declare context: React.ContextType<typeof ApplyLocaleContext>
67
71
  static propTypes = propTypes
68
72
  static allowedProps = allowedProps
69
73
  static defaultProps = {
@@ -78,10 +82,33 @@ class DateInput extends Component<DateInputProps> {
78
82
  isShowingCalendar: false
79
83
  }
80
84
 
81
- state = { hasInputRef: false }
85
+ state = {
86
+ hasInputRef: false,
87
+ isShowingCalendar: false,
88
+ validatedDate: undefined,
89
+ messages: []
90
+ }
82
91
  _input?: HTMLInputElement | null = undefined
83
92
  ref: Element | null = null
84
93
 
94
+ locale(): string {
95
+ if (this.props.locale) {
96
+ return this.props.locale
97
+ } else if (this.context && this.context.locale) {
98
+ return this.context.locale
99
+ }
100
+ return Locale.browserLocale()
101
+ }
102
+
103
+ timezone() {
104
+ if (this.props.timezone) {
105
+ return this.props.timezone
106
+ } else if (this.context && this.context.timezone) {
107
+ return this.context.timezone
108
+ }
109
+ return DateTime.browserTimeZone()
110
+ }
111
+
85
112
  componentDidMount() {
86
113
  this.props.makeStyles?.()
87
114
  }
@@ -127,23 +154,139 @@ class DateInput extends Component<DateInputProps> {
127
154
  }
128
155
 
129
156
  handleShowCalendar = (event: React.SyntheticEvent) => {
130
- if (this.interaction === 'enabled') {
157
+ if (!this.props.children) {
158
+ this.setState({ isShowingCalendar: true })
159
+ } else if (this.interaction === 'enabled' && this.props.children) {
131
160
  this.props.onRequestShowCalendar?.(event)
132
161
  }
133
162
  }
134
163
 
135
- handleHideCalendar = (event: React.SyntheticEvent) => {
136
- this.props.onRequestValidateDate?.(event)
137
- this.props.onRequestHideCalendar?.(event)
164
+ validateDate = (date: string) => {
165
+ const { invalidDateErrorMessage } = this.props
166
+ const disabledDateErrorMessage =
167
+ this.props.disabledDateErrorMessage || invalidDateErrorMessage
168
+ const messages: FormMessage[] = []
169
+ // check if date is enabled
170
+ const { disabledDates } = this.props
171
+ if (
172
+ (typeof disabledDates === 'function' && disabledDates(date)) ||
173
+ (Array.isArray(disabledDates) &&
174
+ disabledDates.find((dateString) =>
175
+ DateTime.parse(dateString, this.locale(), this.timezone()).isSame(
176
+ DateTime.parse(date, this.locale(), this.timezone()),
177
+ 'day'
178
+ )
179
+ ))
180
+ ) {
181
+ messages.push(
182
+ typeof disabledDateErrorMessage === 'function'
183
+ ? disabledDateErrorMessage(date)
184
+ : { type: 'error', text: disabledDateErrorMessage }
185
+ )
186
+ }
187
+
188
+ // check if date is valid
189
+ if (
190
+ !DateTime.parse(
191
+ date,
192
+ this.locale(),
193
+ this.timezone(),
194
+ [
195
+ DateTime.momentISOFormat,
196
+ 'llll',
197
+ 'LLLL',
198
+ 'lll',
199
+ 'LLL',
200
+ 'll',
201
+ 'LL',
202
+ 'l',
203
+ 'L'
204
+ ],
205
+ true
206
+ ).isValid()
207
+ ) {
208
+ messages.push(
209
+ typeof invalidDateErrorMessage === 'function'
210
+ ? invalidDateErrorMessage(date)
211
+ : { type: 'error', text: invalidDateErrorMessage }
212
+ )
213
+ }
214
+
215
+ return messages
216
+ }
217
+
218
+ handleHideCalendar = (event: React.SyntheticEvent, setectedDate?: string) => {
219
+ if (!this.props.children) {
220
+ const dateString = setectedDate || this.props.value
221
+ const messages: FormMessage[] = []
222
+ if (this.props.onRequestValidateDate) {
223
+ const userValidatedDate = this.props.onRequestValidateDate?.(
224
+ event,
225
+ dateString || '',
226
+ this.validateDate(dateString || '')
227
+ )
228
+ messages.push(...(userValidatedDate || []))
229
+ } else {
230
+ if (dateString) {
231
+ messages.push(...this.validateDate(dateString))
232
+ }
233
+ }
234
+ this.setState({ messages, isShowingCalendar: false })
235
+ } else {
236
+ this.props.onRequestValidateDate?.(event)
237
+ this.props.onRequestHideCalendar?.(event)
238
+ }
138
239
  }
139
240
 
140
241
  handleHighlightOption: SelectableProps['onRequestHighlightOption'] = (
141
242
  event,
142
243
  { direction }
143
244
  ) => {
144
- const { onRequestSelectNextDay, onRequestSelectPrevDay } = this.props
145
- if (direction === -1) onRequestSelectPrevDay?.(event)
146
- if (direction === 1) onRequestSelectNextDay?.(event)
245
+ const {
246
+ onRequestSelectNextDay,
247
+ onRequestSelectPrevDay,
248
+ onChange,
249
+ value,
250
+ currentDate
251
+ } = this.props
252
+
253
+ const isValueValid =
254
+ value && DateTime.parse(value, this.locale(), this.timezone()).isValid()
255
+
256
+ if (direction === -1) {
257
+ if (onRequestSelectPrevDay) {
258
+ onRequestSelectPrevDay?.(event)
259
+ } else {
260
+ // @ts-expect-error TODO
261
+ onChange(event, {
262
+ value: DateTime.parse(
263
+ isValueValid ? value : currentDate!,
264
+ this.locale(),
265
+ this.timezone()
266
+ )
267
+ .subtract(1, 'day')
268
+ .format('MMMM D, YYYY')
269
+ })
270
+ this.setState({ messages: [] })
271
+ }
272
+ }
273
+ if (direction === 1) {
274
+ if (onRequestSelectNextDay) {
275
+ onRequestSelectNextDay?.(event)
276
+ } else {
277
+ // @ts-expect-error TODO
278
+ onChange(event, {
279
+ value: DateTime.parse(
280
+ isValueValid ? value : currentDate!,
281
+ this.locale(),
282
+ this.timezone()
283
+ )
284
+ .add(1, 'day')
285
+ .format('MMMM D, YYYY')
286
+ })
287
+ this.setState({ messages: [] })
288
+ }
289
+ }
147
290
  }
148
291
 
149
292
  renderMonthNavigationButton(type = 'prev') {
@@ -155,7 +298,7 @@ class DateInput extends Component<DateInputProps> {
155
298
 
156
299
  renderDays(getOptionProps: SelectableRender['getOptionProps']) {
157
300
  const children = this.props.children as ReactElement<CalendarDayProps>[]
158
-
301
+ if (!children) return
159
302
  return Children.map(children, (day) => {
160
303
  const { date, isOutsideMonth } = day.props
161
304
  const props = { tabIndex: -1, id: this.formatDateId(date) }
@@ -184,8 +327,39 @@ class DateInput extends Component<DateInputProps> {
184
327
  onRequestRenderNextMonth,
185
328
  onRequestRenderPrevMonth,
186
329
  renderNavigationLabel,
187
- renderWeekdayLabels
330
+ renderWeekdayLabels,
331
+ value,
332
+ onChange,
333
+ disabledDates,
334
+ currentDate
188
335
  } = this.props
336
+
337
+ const isValidDate = value
338
+ ? DateTime.parse(value, this.locale(), this.timezone()).isValid()
339
+ : false
340
+
341
+ const noChildrenProps = this.props.children
342
+ ? {}
343
+ : {
344
+ disabledDates,
345
+ currentDate,
346
+ selectedDate: isValidDate ? value : undefined,
347
+ visibleMonth: isValidDate ? value : undefined,
348
+ onDateSelected: (
349
+ dateString: string,
350
+ momentDate: any,
351
+ e: React.MouseEvent
352
+ ) => {
353
+ // @ts-expect-error TODO
354
+ onChange?.(e, {
355
+ value: `${momentDate.format('MMMM')} ${momentDate.format(
356
+ 'D'
357
+ )}, ${momentDate.format('YYYY')}`
358
+ })
359
+ this.handleHideCalendar(e, dateString)
360
+ }
361
+ }
362
+
189
363
  return (
190
364
  <Calendar
191
365
  {...(getListProps({
@@ -196,6 +370,7 @@ class DateInput extends Component<DateInputProps> {
196
370
  renderNextMonthButton: this.renderMonthNavigationButton('next'),
197
371
  renderPrevMonthButton: this.renderMonthNavigationButton('prev')
198
372
  }) as CalendarProps)}
373
+ {...noChildrenProps}
199
374
  >
200
375
  {this.renderDays(getOptionProps)}
201
376
  </Calendar>
@@ -219,7 +394,6 @@ class DateInput extends Component<DateInputProps> {
219
394
  isInline,
220
395
  layout,
221
396
  width,
222
- messages,
223
397
  onRequestValidateDate,
224
398
  onRequestShowCalendar,
225
399
  onRequestHideCalendar,
@@ -236,7 +410,7 @@ class DateInput extends Component<DateInputProps> {
236
410
  ref, // Apply this to the actual inputRef
237
411
  ...triggerProps
238
412
  } = getTriggerProps()
239
-
413
+ const messages = this.props.messages || this.state.messages
240
414
  return (
241
415
  <TextInput
242
416
  {...triggerProps}
@@ -257,20 +431,34 @@ class DateInput extends Component<DateInputProps> {
257
431
  display: isInline ? 'inline-block' : 'block',
258
432
  renderAfterInput: <IconCalendarMonthLine inline={false} />
259
433
  })}
434
+ onKeyDown={(e) => {
435
+ if (!this.props.children) {
436
+ if (e.key === 'Enter') {
437
+ // @ts-expect-error TODO
438
+ this.handleHideCalendar(e)
439
+ }
440
+ }
441
+ triggerProps.onKeyDown?.(e)
442
+ }}
260
443
  />
261
444
  )
262
445
  }
263
446
 
264
- render() {
265
- const { placement, isShowingCalendar, assistiveText, styles } = this.props
447
+ shouldShowCalendar = () =>
448
+ this.props.children
449
+ ? this.props.isShowingCalendar
450
+ : this.state.isShowingCalendar
266
451
 
452
+ render() {
453
+ const { placement, assistiveText, styles } = this.props
454
+ const isShowingCalendar = this.shouldShowCalendar()
267
455
  return (
268
456
  <Selectable
269
457
  isShowingOptions={isShowingCalendar}
270
458
  onRequestShowOptions={this.handleShowCalendar}
271
459
  onRequestHideOptions={this.handleHideCalendar}
272
460
  onRequestHighlightOption={this.handleHighlightOption}
273
- onRequestSelectOption={this.handleHideCalendar}
461
+ onRequestSelectOption={(e) => this.handleHideCalendar(e)}
274
462
  selectedOptionId={this.selectedDateId}
275
463
  highlightedOptionId={this.selectedDateId}
276
464
  >
@@ -132,9 +132,14 @@ type DateInputOwnProps = {
132
132
  /**
133
133
  * Callback fired when the input is blurred. Feedback should be provided
134
134
  * to the user when this function is called if the selected date or input
135
- * value is not valid.
135
+ * value is not valid. The component calculates date validity and if it's
136
+ * disabled or nor and passes that information to this callback.
136
137
  */
137
- onRequestValidateDate?: (event: SyntheticEvent) => void
138
+ onRequestValidateDate?: (
139
+ event: SyntheticEvent,
140
+ dateString?: string,
141
+ validation?: FormMessage[]
142
+ ) => void | FormMessage[]
138
143
  /**
139
144
  * Callback fired requesting the calendar be shown.
140
145
  */
@@ -175,7 +180,7 @@ type DateInputOwnProps = {
175
180
  * full day name for assistive technologies and the children containing the
176
181
  * abbreviation. ex. `[<AccessibleContent alt="Sunday">Sun</AccessibleContent>, ...]`
177
182
  */
178
- renderWeekdayLabels: (React.ReactNode | (() => React.ReactNode))[]
183
+ renderWeekdayLabels?: (React.ReactNode | (() => React.ReactNode))[]
179
184
  /**
180
185
  * A button to render in the calendar navigation header. The recommendation is
181
186
  * to compose it with the [Button](#Button) component, setting the `variant`
@@ -195,6 +200,56 @@ type DateInputOwnProps = {
195
200
  * weeks).
196
201
  */
197
202
  children?: ReactElement<CalendarDayProps>[] // TODO: oneOf([Calendar.Day])
203
+ /*
204
+ * Specify which date(s) will be shown as disabled in the calendar.
205
+ * You can either supply an array of ISO8601 timeDate strings or
206
+ * a function that will be called for each date shown in the calendar.
207
+ */
208
+ disabledDates?: string[] | ((isoDateToCheck: string) => boolean)
209
+ /**
210
+ * ISO date string for the current date if necessary. Defaults to the current
211
+ * date in the user's timezone.
212
+ */
213
+ currentDate?: string
214
+ /**
215
+ * The message shown to the user when the data is invalid.
216
+ * If a string, shown to the user anytime the input is invalid.
217
+ *
218
+ * If a function, receives a single parameter:
219
+ * - *rawDateValue*: the string entered as a date by the user.
220
+ **/
221
+ invalidDateErrorMessage?: string | ((rawDateValue: string) => FormMessage)
222
+ /**
223
+ * Error message shown to the user if they enter a date that is disabled.
224
+ * If not specified the component will show the `invalidDateTimeMessage`.
225
+ */
226
+ disabledDateErrorMessage?: string | ((rawDateValue: string) => FormMessage)
227
+ /**
228
+ * A standard language identifier.
229
+ *
230
+ * See [Moment.js](https://momentjs.com/timezone/docs/#/using-timezones/parsing-in-zone/) for
231
+ * more details.
232
+ *
233
+ * This property can also be set via a context property and if both are set
234
+ * then the component property takes precedence over the context property.
235
+ *
236
+ * The web browser's locale will be used if no value is set via a component
237
+ * property or a context property.
238
+ **/
239
+ locale?: string
240
+ /**
241
+ * A timezone identifier in the format: *Area/Location*
242
+ *
243
+ * See [List of tz database time zones](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) for the list
244
+ * of possible options.
245
+ *
246
+ * This property can also be set via a context property and if both are set
247
+ * then the component property takes precedence over the context property.
248
+ *
249
+ * The web browser's timezone will be used if no value is set via a component
250
+ * property or a context property.
251
+ **/
252
+ timezone?: string
198
253
  }
199
254
 
200
255
  type PropKeys = keyof DateInputOwnProps
@@ -238,10 +293,22 @@ const propTypes: PropValidators<PropKeys> = {
238
293
  renderNavigationLabel: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
239
294
  renderWeekdayLabels: PropTypes.arrayOf(
240
295
  PropTypes.oneOfType([PropTypes.func, PropTypes.node])
241
- ).isRequired,
296
+ ),
242
297
  renderNextMonthButton: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
243
298
  renderPrevMonthButton: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
244
- children: ChildrenPropTypes.oneOf([Calendar.Day])
299
+ children: ChildrenPropTypes.oneOf([Calendar.Day]),
300
+ disabledDates: PropTypes.oneOfType([PropTypes.array, PropTypes.func]),
301
+ currentDate: PropTypes.string,
302
+ disabledDateErrorMessage: PropTypes.oneOfType([
303
+ PropTypes.func,
304
+ PropTypes.string
305
+ ]),
306
+ invalidDateErrorMessage: PropTypes.oneOfType([
307
+ PropTypes.func,
308
+ PropTypes.string
309
+ ]),
310
+ locale: PropTypes.string,
311
+ timezone: PropTypes.string
245
312
  }
246
313
 
247
314
  const allowedProps: AllowedPropKeys = [
@@ -273,8 +340,21 @@ const allowedProps: AllowedPropKeys = [
273
340
  'renderWeekdayLabels',
274
341
  'renderNextMonthButton',
275
342
  'renderPrevMonthButton',
276
- 'children'
343
+ 'children',
344
+ 'disabledDates',
345
+ 'currentDate',
346
+ 'disabledDateErrorMessage',
347
+ 'invalidDateErrorMessage',
348
+ 'locale',
349
+ 'timezone'
277
350
  ]
278
351
 
279
- export type { DateInputProps, DateInputStyle }
352
+ type DateInputState = {
353
+ hasInputRef: boolean
354
+ isShowingCalendar: boolean
355
+ validatedDate?: string
356
+ messages: FormMessage[]
357
+ }
358
+
359
+ export type { DateInputProps, DateInputStyle, DateInputState }
280
360
  export { propTypes, allowedProps }
@@ -7,20 +7,53 @@
7
7
  },
8
8
  "include": ["src"],
9
9
  "references": [
10
- { "path": "../ui-babel-preset/tsconfig.build.json" },
11
- { "path": "../ui-test-locator/tsconfig.build.json" },
12
- { "path": "../ui-test-utils/tsconfig.build.json" },
13
- { "path": "../emotion/tsconfig.build.json" },
14
- { "path": "../ui-calendar/tsconfig.build.json" },
15
- { "path": "../ui-form-field/tsconfig.build.json" },
16
- { "path": "../ui-icons/tsconfig.build.json" },
17
- { "path": "../ui-popover/tsconfig.build.json" },
18
- { "path": "../ui-position/tsconfig.build.json" },
19
- { "path": "../ui-prop-types/tsconfig.build.json" },
20
- { "path": "../ui-react-utils/tsconfig.build.json" },
21
- { "path": "../ui-selectable/tsconfig.build.json" },
22
- { "path": "../ui-testable/tsconfig.build.json" },
23
- { "path": "../ui-text-input/tsconfig.build.json" },
24
- { "path": "../ui-utils/tsconfig.build.json" }
10
+ {
11
+ "path": "../ui-babel-preset/tsconfig.build.json"
12
+ },
13
+ {
14
+ "path": "../ui-test-locator/tsconfig.build.json"
15
+ },
16
+ {
17
+ "path": "../ui-test-utils/tsconfig.build.json"
18
+ },
19
+ {
20
+ "path": "../emotion/tsconfig.build.json"
21
+ },
22
+ {
23
+ "path": "../ui-calendar/tsconfig.build.json"
24
+ },
25
+ {
26
+ "path": "../ui-form-field/tsconfig.build.json"
27
+ },
28
+ {
29
+ "path": "../ui-icons/tsconfig.build.json"
30
+ },
31
+ {
32
+ "path": "../ui-popover/tsconfig.build.json"
33
+ },
34
+ {
35
+ "path": "../ui-position/tsconfig.build.json"
36
+ },
37
+ {
38
+ "path": "../ui-prop-types/tsconfig.build.json"
39
+ },
40
+ {
41
+ "path": "../ui-react-utils/tsconfig.build.json"
42
+ },
43
+ {
44
+ "path": "../ui-selectable/tsconfig.build.json"
45
+ },
46
+ {
47
+ "path": "../ui-testable/tsconfig.build.json"
48
+ },
49
+ {
50
+ "path": "../ui-text-input/tsconfig.build.json"
51
+ },
52
+ {
53
+ "path": "../ui-utils/tsconfig.build.json"
54
+ },
55
+ {
56
+ "path": "../ui-i18n/tsconfig.build.json"
57
+ }
25
58
  ]
26
59
  }