@instructure/ui-date-input 10.16.1 → 10.16.3

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.
@@ -22,7 +22,14 @@
22
22
  * SOFTWARE.
23
23
  */
24
24
 
25
- import { useState, useEffect, useContext } from 'react'
25
+ import {
26
+ useState,
27
+ useEffect,
28
+ useContext,
29
+ forwardRef,
30
+ ForwardedRef,
31
+ ValidationMap
32
+ } from 'react'
26
33
  import type { SyntheticEvent } from 'react'
27
34
  import { Calendar } from '@instructure/ui-calendar'
28
35
  import { IconButton } from '@instructure/ui-buttons'
@@ -130,224 +137,235 @@ function parseLocaleDate(
130
137
  category: components
131
138
  ---
132
139
  **/
133
- const DateInput2 = ({
134
- renderLabel,
135
- screenReaderLabels,
136
- isRequired = false,
137
- interaction = 'enabled',
138
- isInline = false,
139
- value,
140
- messages,
141
- width,
142
- onChange,
143
- onBlur,
144
- withYearPicker,
145
- invalidDateErrorMessage,
146
- locale,
147
- timezone,
148
- placeholder,
149
- dateFormat,
150
- onRequestValidateDate,
151
- disabledDates,
152
- renderCalendarIcon,
153
- margin,
154
- inputRef,
155
- ...rest
156
- }: DateInput2Props) => {
157
- const localeContext = useContext(ApplyLocaleContext)
158
-
159
- const getLocale = () => {
160
- if (locale) {
161
- return locale
162
- } else if (localeContext.locale) {
163
- return localeContext.locale
164
- }
165
- // default to the system's locale
166
- return Locale.browserLocale()
167
- }
140
+ // eslint-disable-next-line react/display-name
141
+ const DateInput2 = forwardRef(
142
+ (
143
+ {
144
+ renderLabel,
145
+ screenReaderLabels,
146
+ isRequired = false,
147
+ interaction = 'enabled',
148
+ isInline = false,
149
+ value,
150
+ messages,
151
+ width,
152
+ onChange,
153
+ onBlur,
154
+ withYearPicker,
155
+ invalidDateErrorMessage,
156
+ locale,
157
+ timezone,
158
+ placeholder,
159
+ dateFormat,
160
+ onRequestValidateDate,
161
+ disabledDates,
162
+ renderCalendarIcon,
163
+ margin,
164
+ inputRef,
165
+ ...rest
166
+ }: DateInput2Props,
167
+ ref: ForwardedRef<TextInput>
168
+ ) => {
169
+ const localeContext = useContext(ApplyLocaleContext)
168
170
 
169
- const getTimezone = () => {
170
- if (timezone) {
171
- return timezone
172
- } else if (localeContext.timezone) {
173
- return localeContext.timezone
171
+ const getLocale = () => {
172
+ if (locale) {
173
+ return locale
174
+ } else if (localeContext.locale) {
175
+ return localeContext.locale
176
+ }
177
+ // default to the system's locale
178
+ return Locale.browserLocale()
174
179
  }
175
- // default to the system's timezone
176
- return Intl.DateTimeFormat().resolvedOptions().timeZone
177
- }
178
180
 
179
- const [inputMessages, setInputMessages] = useState<FormMessage[]>(
180
- messages || []
181
- )
182
- const [showPopover, setShowPopover] = useState<boolean>(false)
181
+ const getTimezone = () => {
182
+ if (timezone) {
183
+ return timezone
184
+ } else if (localeContext.timezone) {
185
+ return localeContext.timezone
186
+ }
187
+ // default to the system's timezone
188
+ return Intl.DateTimeFormat().resolvedOptions().timeZone
189
+ }
183
190
 
184
- useEffect(() => {
185
- // don't set input messages if there is an internal error set already
186
- if (inputMessages.find((m) => m.text === invalidDateErrorMessage)) return
191
+ const [inputMessages, setInputMessages] = useState<FormMessage[]>(
192
+ messages || []
193
+ )
194
+ const [showPopover, setShowPopover] = useState<boolean>(false)
187
195
 
188
- setInputMessages(messages || [])
189
- }, [messages])
196
+ useEffect(() => {
197
+ // don't set input messages if there is an internal error set already
198
+ if (inputMessages.find((m) => m.text === invalidDateErrorMessage)) return
190
199
 
191
- useEffect(() => {
192
- const [, utcIsoDate] = parseDate(value)
193
- // clear error messages if date becomes valid
194
- if (utcIsoDate || !value) {
195
200
  setInputMessages(messages || [])
196
- }
197
- }, [value])
198
-
199
- const parseDate = (dateString: string = ''): [string, string] => {
200
- let date: Date | null = null
201
- if (dateFormat) {
202
- if (typeof dateFormat === 'string') {
203
- // use dateFormat instead of the user locale
204
- date = parseLocaleDate(dateString, dateFormat, getTimezone())
205
- } else if (dateFormat.parser) {
206
- date = dateFormat.parser(dateString)
201
+ }, [messages])
202
+
203
+ useEffect(() => {
204
+ const [, utcIsoDate] = parseDate(value)
205
+ // clear error messages if date becomes valid
206
+ if (utcIsoDate || !value) {
207
+ setInputMessages(messages || [])
208
+ }
209
+ }, [value])
210
+
211
+ const parseDate = (dateString: string = ''): [string, string] => {
212
+ let date: Date | null = null
213
+ if (dateFormat) {
214
+ if (typeof dateFormat === 'string') {
215
+ // use dateFormat instead of the user locale
216
+ date = parseLocaleDate(dateString, dateFormat, getTimezone())
217
+ } else if (dateFormat.parser) {
218
+ date = dateFormat.parser(dateString)
219
+ }
220
+ } else {
221
+ // no dateFormat prop passed, use locale for formatting
222
+ date = parseLocaleDate(dateString, getLocale(), getTimezone())
207
223
  }
208
- } else {
209
- // no dateFormat prop passed, use locale for formatting
210
- date = parseLocaleDate(dateString, getLocale(), getTimezone())
224
+ return date ? [formatDate(date), date.toISOString()] : ['', '']
211
225
  }
212
- return date ? [formatDate(date), date.toISOString()] : ['', '']
213
- }
214
226
 
215
- const formatDate = (date: Date): string => {
216
- // use formatter function if provided
217
- if (typeof dateFormat !== 'string' && dateFormat?.formatter) {
218
- return dateFormat.formatter(date)
227
+ const formatDate = (date: Date): string => {
228
+ // use formatter function if provided
229
+ if (typeof dateFormat !== 'string' && dateFormat?.formatter) {
230
+ return dateFormat.formatter(date)
231
+ }
232
+ // if dateFormat set to a locale, use that, otherwise default to the user's locale
233
+ return date.toLocaleDateString(
234
+ typeof dateFormat === 'string' ? dateFormat : getLocale(),
235
+ {
236
+ timeZone: getTimezone(),
237
+ calendar: 'gregory',
238
+ numberingSystem: 'latn'
239
+ }
240
+ )
219
241
  }
220
- // if dateFormat set to a locale, use that, otherwise default to the user's locale
221
- return date.toLocaleDateString(
222
- typeof dateFormat === 'string' ? dateFormat : getLocale(),
223
- { timeZone: getTimezone(), calendar: 'gregory', numberingSystem: 'latn' }
224
- )
225
- }
226
242
 
227
- const getDateFromatHint = () => {
228
- const exampleDate = new Date('2024-09-01')
229
- const formattedDate = formatDate(exampleDate)
243
+ const getDateFromatHint = () => {
244
+ const exampleDate = new Date('2024-09-01')
245
+ const formattedDate = formatDate(exampleDate)
230
246
 
231
- // Create a regular expression to find the exact match of the number
232
- const regex = (n: string) => {
233
- return new RegExp(`(?<!\\d)0*${n}(?!\\d)`, 'g')
234
- }
247
+ // Create a regular expression to find the exact match of the number
248
+ const regex = (n: string) => {
249
+ return new RegExp(`(?<!\\d)0*${n}(?!\\d)`, 'g')
250
+ }
235
251
 
236
- // Replace the matched number with the same number of dashes
237
- const year = `${exampleDate.getFullYear()}`
238
- const month = `${exampleDate.getMonth() + 1}`
239
- const day = `${exampleDate.getDate()}`
240
- return formattedDate
241
- .replace(regex(year), (match) => 'Y'.repeat(match.length))
242
- .replace(regex(month), (match) => 'M'.repeat(match.length))
243
- .replace(regex(day), (match) => 'D'.repeat(match.length))
244
- }
252
+ // Replace the matched number with the same number of dashes
253
+ const year = `${exampleDate.getFullYear()}`
254
+ const month = `${exampleDate.getMonth() + 1}`
255
+ const day = `${exampleDate.getDate()}`
256
+ return formattedDate
257
+ .replace(regex(year), (match) => 'Y'.repeat(match.length))
258
+ .replace(regex(month), (match) => 'M'.repeat(match.length))
259
+ .replace(regex(day), (match) => 'D'.repeat(match.length))
260
+ }
245
261
 
246
- const handleInputChange = (e: SyntheticEvent, newValue: string) => {
247
- const [, utcIsoDate] = parseDate(newValue)
248
- onChange?.(e, newValue, utcIsoDate)
249
- }
262
+ const handleInputChange = (e: SyntheticEvent, newValue: string) => {
263
+ const [, utcIsoDate] = parseDate(newValue)
264
+ onChange?.(e, newValue, utcIsoDate)
265
+ }
250
266
 
251
- const handleDateSelected = (
252
- dateString: string,
253
- _momentDate: Moment,
254
- e: SyntheticEvent
255
- ) => {
256
- setShowPopover(false)
257
- const newValue = formatDate(new Date(dateString))
258
- onChange?.(e, newValue, dateString)
259
- onRequestValidateDate?.(e, newValue, dateString)
260
- }
267
+ const handleDateSelected = (
268
+ dateString: string,
269
+ _momentDate: Moment,
270
+ e: SyntheticEvent
271
+ ) => {
272
+ setShowPopover(false)
273
+ const newValue = formatDate(new Date(dateString))
274
+ onChange?.(e, newValue, dateString)
275
+ onRequestValidateDate?.(e, newValue, dateString)
276
+ }
261
277
 
262
- const handleBlur = (e: SyntheticEvent) => {
263
- const [localeDate, utcIsoDate] = parseDate(value)
264
- if (localeDate) {
265
- if (localeDate !== value) {
266
- onChange?.(e, localeDate, utcIsoDate)
278
+ const handleBlur = (e: SyntheticEvent) => {
279
+ const [localeDate, utcIsoDate] = parseDate(value)
280
+ if (localeDate) {
281
+ if (localeDate !== value) {
282
+ onChange?.(e, localeDate, utcIsoDate)
283
+ }
284
+ } else if (value && invalidDateErrorMessage) {
285
+ setInputMessages([{ type: 'error', text: invalidDateErrorMessage }])
267
286
  }
268
- } else if (value && invalidDateErrorMessage) {
269
- setInputMessages([{ type: 'error', text: invalidDateErrorMessage }])
287
+ onRequestValidateDate?.(e, value || '', utcIsoDate)
288
+ onBlur?.(e, value || '', utcIsoDate)
270
289
  }
271
- onRequestValidateDate?.(e, value || '', utcIsoDate)
272
- onBlur?.(e, value || '', utcIsoDate)
273
- }
274
290
 
275
- const selectedDate = parseDate(value)[1]
276
-
277
- return (
278
- <TextInput
279
- {...passthroughProps(rest)}
280
- inputRef={inputRef}
281
- renderLabel={renderLabel}
282
- onChange={handleInputChange}
283
- onBlur={handleBlur}
284
- isRequired={isRequired}
285
- value={value}
286
- placeholder={placeholder ?? getDateFromatHint()}
287
- width={width}
288
- display={isInline ? 'inline-block' : 'block'}
289
- messages={inputMessages}
290
- interaction={interaction}
291
- margin={margin}
292
- renderAfterInput={
293
- <Popover
294
- renderTrigger={
295
- <IconButton
296
- withBackground={false}
297
- withBorder={false}
298
- screenReaderLabel={screenReaderLabels.calendarIcon}
299
- shape="circle"
300
- interaction={interaction}
301
- >
302
- {renderCalendarIcon ? (
303
- callRenderProp(renderCalendarIcon)
304
- ) : (
305
- <IconCalendarMonthLine />
306
- )}
307
- </IconButton>
308
- }
309
- isShowingContent={showPopover}
310
- onShowContent={() => setShowPopover(true)}
311
- onHideContent={() => setShowPopover(false)}
312
- on="click"
313
- shouldContainFocus
314
- shouldReturnFocus
315
- shouldCloseOnDocumentClick
316
- >
317
- <Calendar
318
- withYearPicker={withYearPicker}
319
- onDateSelected={handleDateSelected}
320
- selectedDate={selectedDate}
321
- disabledDates={disabledDates}
322
- visibleMonth={selectedDate}
323
- locale={getLocale()}
324
- timezone={getTimezone()}
325
- renderNextMonthButton={
291
+ const selectedDate = parseDate(value)[1]
292
+
293
+ return (
294
+ <TextInput
295
+ {...passthroughProps(rest)}
296
+ ref={ref}
297
+ inputRef={inputRef}
298
+ renderLabel={renderLabel}
299
+ onChange={handleInputChange}
300
+ onBlur={handleBlur}
301
+ isRequired={isRequired}
302
+ value={value}
303
+ placeholder={placeholder ?? getDateFromatHint()}
304
+ width={width}
305
+ display={isInline ? 'inline-block' : 'block'}
306
+ messages={inputMessages}
307
+ interaction={interaction}
308
+ margin={margin}
309
+ renderAfterInput={
310
+ <Popover
311
+ renderTrigger={
326
312
  <IconButton
327
- size="small"
328
313
  withBackground={false}
329
314
  withBorder={false}
330
- renderIcon={<IconArrowOpenEndSolid color="primary" />}
331
- screenReaderLabel={screenReaderLabels.nextMonthButton}
332
- />
315
+ screenReaderLabel={screenReaderLabels.calendarIcon}
316
+ shape="circle"
317
+ interaction={interaction}
318
+ >
319
+ {renderCalendarIcon ? (
320
+ callRenderProp(renderCalendarIcon)
321
+ ) : (
322
+ <IconCalendarMonthLine />
323
+ )}
324
+ </IconButton>
333
325
  }
334
- renderPrevMonthButton={
335
- <IconButton
336
- size="small"
337
- withBackground={false}
338
- withBorder={false}
339
- renderIcon={<IconArrowOpenStartSolid color="primary" />}
340
- screenReaderLabel={screenReaderLabels.prevMonthButton}
341
- />
342
- }
343
- />
344
- </Popover>
345
- }
346
- />
347
- )
348
- }
326
+ isShowingContent={showPopover}
327
+ onShowContent={() => setShowPopover(true)}
328
+ onHideContent={() => setShowPopover(false)}
329
+ on="click"
330
+ shouldContainFocus
331
+ shouldReturnFocus
332
+ shouldCloseOnDocumentClick
333
+ >
334
+ <Calendar
335
+ withYearPicker={withYearPicker}
336
+ onDateSelected={handleDateSelected}
337
+ selectedDate={selectedDate}
338
+ disabledDates={disabledDates}
339
+ visibleMonth={selectedDate}
340
+ locale={getLocale()}
341
+ timezone={getTimezone()}
342
+ renderNextMonthButton={
343
+ <IconButton
344
+ size="small"
345
+ withBackground={false}
346
+ withBorder={false}
347
+ renderIcon={<IconArrowOpenEndSolid color="primary" />}
348
+ screenReaderLabel={screenReaderLabels.nextMonthButton}
349
+ />
350
+ }
351
+ renderPrevMonthButton={
352
+ <IconButton
353
+ size="small"
354
+ withBackground={false}
355
+ withBorder={false}
356
+ renderIcon={<IconArrowOpenStartSolid color="primary" />}
357
+ screenReaderLabel={screenReaderLabels.prevMonthButton}
358
+ />
359
+ }
360
+ />
361
+ </Popover>
362
+ }
363
+ />
364
+ )
365
+ }
366
+ )
349
367
 
350
- DateInput2.propTypes = propTypes
368
+ DateInput2.propTypes = propTypes as ValidationMap<DateInput2Props>
351
369
 
352
370
  export default DateInput2
353
371
  export { DateInput2 }