@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.
- package/CHANGELOG.md +19 -0
- package/es/DateInput2/__new-tests__/DateInput2.test.js +30 -8
- package/es/DateInput2/index.js +6 -4
- package/lib/DateInput2/__new-tests__/DateInput2.test.js +30 -8
- package/lib/DateInput2/index.js +5 -4
- package/package.json +20 -20
- package/src/DateInput2/__new-tests__/DateInput2.test.tsx +23 -0
- package/src/DateInput2/index.tsx +212 -194
- package/tsconfig.build.tsbuildinfo +1 -1
- package/types/DateInput2/index.d.ts +106 -39
- package/types/DateInput2/index.d.ts.map +1 -1
package/src/DateInput2/index.tsx
CHANGED
@@ -22,7 +22,14 @@
|
|
22
22
|
* SOFTWARE.
|
23
23
|
*/
|
24
24
|
|
25
|
-
import {
|
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
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
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
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
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
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
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
|
-
|
185
|
-
|
186
|
-
|
191
|
+
const [inputMessages, setInputMessages] = useState<FormMessage[]>(
|
192
|
+
messages || []
|
193
|
+
)
|
194
|
+
const [showPopover, setShowPopover] = useState<boolean>(false)
|
187
195
|
|
188
|
-
|
189
|
-
|
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
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
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
|
-
|
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
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
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
|
-
|
228
|
-
|
229
|
-
|
243
|
+
const getDateFromatHint = () => {
|
244
|
+
const exampleDate = new Date('2024-09-01')
|
245
|
+
const formattedDate = formatDate(exampleDate)
|
230
246
|
|
231
|
-
|
232
|
-
|
233
|
-
|
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
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
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
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
262
|
+
const handleInputChange = (e: SyntheticEvent, newValue: string) => {
|
263
|
+
const [, utcIsoDate] = parseDate(newValue)
|
264
|
+
onChange?.(e, newValue, utcIsoDate)
|
265
|
+
}
|
250
266
|
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
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
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
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
|
-
|
269
|
-
|
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
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
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
|
-
|
331
|
-
|
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
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
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 }
|