@dhis2-ui/calendar 10.19.0 → 10.19.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.
- package/build/cjs/calendar-input/CLAUDE.md +25 -0
- package/build/cjs/calendar-input/__tests__/calendar-input.test.js +319 -3
- package/build/cjs/calendar-input/calendar-input.js +4 -1
- package/build/es/calendar-input/CLAUDE.md +25 -0
- package/build/es/calendar-input/__tests__/calendar-input.test.js +319 -3
- package/build/es/calendar-input/calendar-input.js +4 -1
- package/package.json +9 -9
- package/src/calendar-input/CLAUDE.md +25 -0
- package/src/calendar-input/__tests__/calendar-input.test.js +447 -3
- package/src/calendar-input/calendar-input.js +4 -1
- package/types/index.d.ts +28 -2
|
@@ -40,6 +40,89 @@ describe('Calendar Input', () => {
|
|
|
40
40
|
jest.useRealTimers()
|
|
41
41
|
})
|
|
42
42
|
|
|
43
|
+
it('allow selection of a date through the calendar widget (ethiopic)', async () => {
|
|
44
|
+
jest.useFakeTimers()
|
|
45
|
+
jest.setSystemTime(new Date('2024-10-22T09:05:00.000Z'))
|
|
46
|
+
|
|
47
|
+
const onDateSelectMock = jest.fn()
|
|
48
|
+
const screen = render(
|
|
49
|
+
<CalendarInput
|
|
50
|
+
calendar="ethiopian"
|
|
51
|
+
onDateSelect={onDateSelectMock}
|
|
52
|
+
/>
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
const dateInput = within(
|
|
56
|
+
screen.getByTestId('dhis2-uicore-input')
|
|
57
|
+
).getByRole('textbox')
|
|
58
|
+
|
|
59
|
+
fireEvent.focus(dateInput)
|
|
60
|
+
|
|
61
|
+
const calendar = await screen.findByTestId('calendar')
|
|
62
|
+
expect(calendar).toBeInTheDocument()
|
|
63
|
+
|
|
64
|
+
const todayString = '2017-02-12'
|
|
65
|
+
const today = within(calendar).getByTestId(todayString)
|
|
66
|
+
|
|
67
|
+
// the visible day-cell text must be the bare day number, not the
|
|
68
|
+
// full date string used for data-test - regressed silently during
|
|
69
|
+
// the multi-calendar-dates 2.2.0-alpha.1 upgrade (see CLAUDE.md)
|
|
70
|
+
expect(within(today).getByText('12')).toBeInTheDocument()
|
|
71
|
+
|
|
72
|
+
fireEvent.click(today)
|
|
73
|
+
|
|
74
|
+
await waitFor(() => {
|
|
75
|
+
expect(calendar).not.toBeInTheDocument()
|
|
76
|
+
})
|
|
77
|
+
expect(onDateSelectMock).toHaveBeenCalledWith(
|
|
78
|
+
expect.objectContaining({
|
|
79
|
+
calendarDateString: todayString,
|
|
80
|
+
})
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
jest.useRealTimers()
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('allow selection of a date through the calendar widget (nepali)', async () => {
|
|
87
|
+
jest.useFakeTimers()
|
|
88
|
+
jest.setSystemTime(new Date('2024-10-22T09:05:00.000Z'))
|
|
89
|
+
|
|
90
|
+
const onDateSelectMock = jest.fn()
|
|
91
|
+
const screen = render(
|
|
92
|
+
<CalendarInput calendar="nepali" onDateSelect={onDateSelectMock} />
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
const dateInput = within(
|
|
96
|
+
screen.getByTestId('dhis2-uicore-input')
|
|
97
|
+
).getByRole('textbox')
|
|
98
|
+
|
|
99
|
+
fireEvent.focus(dateInput)
|
|
100
|
+
|
|
101
|
+
const calendar = await screen.findByTestId('calendar')
|
|
102
|
+
expect(calendar).toBeInTheDocument()
|
|
103
|
+
|
|
104
|
+
const todayString = '2081-07-06'
|
|
105
|
+
const today = within(calendar).getByTestId(todayString)
|
|
106
|
+
|
|
107
|
+
// the visible day-cell text must be the bare day number, not the
|
|
108
|
+
// full date string used for data-test - regressed silently during
|
|
109
|
+
// the multi-calendar-dates 2.2.0-alpha.1 upgrade (see CLAUDE.md)
|
|
110
|
+
expect(within(today).getByText('6')).toBeInTheDocument()
|
|
111
|
+
|
|
112
|
+
fireEvent.click(today)
|
|
113
|
+
|
|
114
|
+
await waitFor(() => {
|
|
115
|
+
expect(calendar).not.toBeInTheDocument()
|
|
116
|
+
})
|
|
117
|
+
expect(onDateSelectMock).toHaveBeenCalledWith(
|
|
118
|
+
expect.objectContaining({
|
|
119
|
+
calendarDateString: todayString,
|
|
120
|
+
})
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
jest.useRealTimers()
|
|
124
|
+
})
|
|
125
|
+
|
|
43
126
|
it('allow selection of a date through the input', async () => {
|
|
44
127
|
const onDateSelectMock = jest.fn()
|
|
45
128
|
const screen = render(
|
|
@@ -61,7 +144,286 @@ describe('Calendar Input', () => {
|
|
|
61
144
|
)
|
|
62
145
|
})
|
|
63
146
|
|
|
147
|
+
it('rejects input whose detected shape does not match the format prop', () => {
|
|
148
|
+
// "2024-12-25" is unambiguously YYYY-MM-DD shaped (4-digit group
|
|
149
|
+
// first). With format="DD-MM-YYYY" that mismatch is rejected
|
|
150
|
+
// rather than reinterpreted.
|
|
151
|
+
const screen = render(
|
|
152
|
+
<CalendarWithValidation calendar="gregory" format="DD-MM-YYYY" />
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
const dateInput = within(
|
|
156
|
+
screen.getByTestId('dhis2-uicore-input')
|
|
157
|
+
).getByRole('textbox')
|
|
158
|
+
|
|
159
|
+
fireEvent.change(dateInput, { target: { value: '2024-12-25' } })
|
|
160
|
+
fireEvent.blur(dateInput)
|
|
161
|
+
|
|
162
|
+
expect(
|
|
163
|
+
screen.getByText(
|
|
164
|
+
'Date string format does not match the specified format. Expected DD-MM-YYYY but got YYYY-MM-DD.'
|
|
165
|
+
)
|
|
166
|
+
).toBeInTheDocument()
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('accepts the same typed input without the format prop', () => {
|
|
170
|
+
const screen = render(<CalendarWithValidation calendar="gregory" />)
|
|
171
|
+
|
|
172
|
+
const dateInput = within(
|
|
173
|
+
screen.getByTestId('dhis2-uicore-input')
|
|
174
|
+
).getByRole('textbox')
|
|
175
|
+
|
|
176
|
+
fireEvent.change(dateInput, { target: { value: '2024-12-25' } })
|
|
177
|
+
fireEvent.blur(dateInput)
|
|
178
|
+
|
|
179
|
+
expect(
|
|
180
|
+
screen.queryByText(/does not match the specified format/)
|
|
181
|
+
).not.toBeInTheDocument()
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('limits the year selector to the current year when pastOnly is set', async () => {
|
|
185
|
+
jest.useFakeTimers()
|
|
186
|
+
jest.setSystemTime(new Date('2024-10-22T09:05:00.000Z'))
|
|
187
|
+
|
|
188
|
+
const screen = render(
|
|
189
|
+
<CalendarInput
|
|
190
|
+
calendar="gregory"
|
|
191
|
+
pastOnly
|
|
192
|
+
onDateSelect={jest.fn()}
|
|
193
|
+
/>
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
const dateInput = within(
|
|
197
|
+
screen.getByTestId('dhis2-uicore-input')
|
|
198
|
+
).getByRole('textbox')
|
|
199
|
+
fireEvent.focus(dateInput)
|
|
200
|
+
|
|
201
|
+
const yearSelect = await screen.findByTestId('calendar-year-select')
|
|
202
|
+
const years = within(yearSelect)
|
|
203
|
+
.getAllByRole('option')
|
|
204
|
+
.map((option) => Number(option.value))
|
|
205
|
+
|
|
206
|
+
expect(Math.max(...years)).toBe(2024)
|
|
207
|
+
|
|
208
|
+
jest.useRealTimers()
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('includes future years in the year selector without pastOnly', async () => {
|
|
212
|
+
jest.useFakeTimers()
|
|
213
|
+
jest.setSystemTime(new Date('2024-10-22T09:05:00.000Z'))
|
|
214
|
+
|
|
215
|
+
const screen = render(
|
|
216
|
+
<CalendarInput calendar="gregory" onDateSelect={jest.fn()} />
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
const dateInput = within(
|
|
220
|
+
screen.getByTestId('dhis2-uicore-input')
|
|
221
|
+
).getByRole('textbox')
|
|
222
|
+
fireEvent.focus(dateInput)
|
|
223
|
+
|
|
224
|
+
const yearSelect = await screen.findByTestId('calendar-year-select')
|
|
225
|
+
const years = within(yearSelect)
|
|
226
|
+
.getAllByRole('option')
|
|
227
|
+
.map((option) => Number(option.value))
|
|
228
|
+
|
|
229
|
+
expect(Math.max(...years)).toBeGreaterThan(2024)
|
|
230
|
+
|
|
231
|
+
jest.useRealTimers()
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
it('sets the calendar wrapper direction from the dir prop', async () => {
|
|
235
|
+
const screen = render(
|
|
236
|
+
<CalendarInput
|
|
237
|
+
calendar="gregory"
|
|
238
|
+
dir="rtl"
|
|
239
|
+
onDateSelect={jest.fn()}
|
|
240
|
+
/>
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
const dateInput = within(
|
|
244
|
+
screen.getByTestId('dhis2-uicore-input')
|
|
245
|
+
).getByRole('textbox')
|
|
246
|
+
fireEvent.focus(dateInput)
|
|
247
|
+
|
|
248
|
+
const calendar = await screen.findByTestId('calendar')
|
|
249
|
+
expect(calendar).toHaveAttribute('dir', 'rtl')
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
it('renders day labels using the numberingSystem prop', async () => {
|
|
253
|
+
jest.useFakeTimers()
|
|
254
|
+
jest.setSystemTime(new Date('2024-10-22T09:05:00.000Z'))
|
|
255
|
+
|
|
256
|
+
const screen = render(
|
|
257
|
+
<CalendarInput
|
|
258
|
+
calendar="gregory"
|
|
259
|
+
numberingSystem="arab"
|
|
260
|
+
onDateSelect={jest.fn()}
|
|
261
|
+
/>
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
const dateInput = within(
|
|
265
|
+
screen.getByTestId('dhis2-uicore-input')
|
|
266
|
+
).getByRole('textbox')
|
|
267
|
+
fireEvent.focus(dateInput)
|
|
268
|
+
|
|
269
|
+
const calendar = await screen.findByTestId('calendar')
|
|
270
|
+
const today = within(calendar).getByTestId('2024-10-22')
|
|
271
|
+
|
|
272
|
+
expect(today).toHaveTextContent('٢٢')
|
|
273
|
+
|
|
274
|
+
jest.useRealTimers()
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
it('renders weekday headers using the weekDayFormat prop', async () => {
|
|
278
|
+
const screen = render(
|
|
279
|
+
<CalendarInput
|
|
280
|
+
calendar="gregory"
|
|
281
|
+
weekDayFormat="long"
|
|
282
|
+
onDateSelect={jest.fn()}
|
|
283
|
+
/>
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
const dateInput = within(
|
|
287
|
+
screen.getByTestId('dhis2-uicore-input')
|
|
288
|
+
).getByRole('textbox')
|
|
289
|
+
fireEvent.focus(dateInput)
|
|
290
|
+
|
|
291
|
+
const calendar = await screen.findByTestId('calendar')
|
|
292
|
+
expect(within(calendar).getByText('Monday')).toBeInTheDocument()
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
// Regression coverage for LIBS-763: useDatePicker (in @dhis2/multi-calendar-dates)
|
|
296
|
+
// used to rebuild the year/month navigation lists and the day-cell grid on every
|
|
297
|
+
// render - including every keystroke while typing here, with the calendar widget
|
|
298
|
+
// open - regardless of whether the visible month/calendar/locale had actually
|
|
299
|
+
// changed. Fixed upstream in @dhis2/multi-calendar-dates@3.0.0-alpha.1.
|
|
300
|
+
describe('performance (LIBS-763)', () => {
|
|
301
|
+
// For gregory/ethiopic (native-ICU calendars) the broken memoization showed
|
|
302
|
+
// up as ~500 redundant `new Intl.DateTimeFormat(...)` constructions for a
|
|
303
|
+
// single keystroke; count is a deterministic, machine-speed-independent
|
|
304
|
+
// signal, unlike timing it directly.
|
|
305
|
+
const countDateTimeFormatConstructions = (fn) => {
|
|
306
|
+
const realResolvedOptions =
|
|
307
|
+
Intl.DateTimeFormat.prototype.resolvedOptions
|
|
308
|
+
let count = 0
|
|
309
|
+
Intl.DateTimeFormat.prototype.resolvedOptions = function (...args) {
|
|
310
|
+
count++
|
|
311
|
+
return realResolvedOptions.apply(this, args)
|
|
312
|
+
}
|
|
313
|
+
try {
|
|
314
|
+
fn()
|
|
315
|
+
} finally {
|
|
316
|
+
Intl.DateTimeFormat.prototype.resolvedOptions =
|
|
317
|
+
realResolvedOptions
|
|
318
|
+
}
|
|
319
|
+
return count
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
it('does not reconstruct Intl.DateTimeFormat instances while typing (gregory)', async () => {
|
|
323
|
+
const screen = render(
|
|
324
|
+
<CalendarInput calendar="gregory" onDateSelect={jest.fn()} />
|
|
325
|
+
)
|
|
326
|
+
const dateInput = within(
|
|
327
|
+
screen.getByTestId('dhis2-uicore-input')
|
|
328
|
+
).getByRole('textbox')
|
|
329
|
+
fireEvent.focus(dateInput)
|
|
330
|
+
await screen.findByTestId('calendar')
|
|
331
|
+
|
|
332
|
+
const constructions = countDateTimeFormatConstructions(() => {
|
|
333
|
+
fireEvent.change(dateInput, {
|
|
334
|
+
target: { value: '2024-10-1' },
|
|
335
|
+
})
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
expect(constructions).toBeLessThan(10)
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
it('does not reconstruct Intl.DateTimeFormat instances while typing (ethiopic)', async () => {
|
|
342
|
+
const screen = render(
|
|
343
|
+
<CalendarInput calendar="ethiopian" onDateSelect={jest.fn()} />
|
|
344
|
+
)
|
|
345
|
+
const dateInput = within(
|
|
346
|
+
screen.getByTestId('dhis2-uicore-input')
|
|
347
|
+
).getByRole('textbox')
|
|
348
|
+
fireEvent.focus(dateInput)
|
|
349
|
+
await screen.findByTestId('calendar')
|
|
350
|
+
|
|
351
|
+
const constructions = countDateTimeFormatConstructions(() => {
|
|
352
|
+
fireEvent.change(dateInput, {
|
|
353
|
+
target: { value: '2017-02-0' },
|
|
354
|
+
})
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
expect(constructions).toBeLessThan(10)
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
// Nepali is a custom (non-ICU) calendar, so it never goes through
|
|
361
|
+
// Intl.DateTimeFormat - the same broken memoization instead showed up as
|
|
362
|
+
// redundant custom-calendar date-conversion work, so time it directly.
|
|
363
|
+
// 300ms for 10 keystrokes is ~25x the fixed baseline (~13ms) and ~2.5x
|
|
364
|
+
// below the broken one (~760ms), leaving headroom in both directions.
|
|
365
|
+
// Skipped in CI: unlike the count-based tests above, this asserts on
|
|
366
|
+
// wall-clock time, which can be flaky on slower/shared CI runners.
|
|
367
|
+
// Uncomment to run locally when touching useDatePicker/useNavigation.
|
|
368
|
+
it.skip('keeps typing fast (nepali)', async () => {
|
|
369
|
+
const screen = render(
|
|
370
|
+
<CalendarInput calendar="nepali" onDateSelect={jest.fn()} />
|
|
371
|
+
)
|
|
372
|
+
const dateInput = within(
|
|
373
|
+
screen.getByTestId('dhis2-uicore-input')
|
|
374
|
+
).getByRole('textbox')
|
|
375
|
+
fireEvent.focus(dateInput)
|
|
376
|
+
await screen.findByTestId('calendar')
|
|
377
|
+
|
|
378
|
+
const chars = '2081-07-01'.split('')
|
|
379
|
+
const progressive = chars.map((_, i) =>
|
|
380
|
+
chars.slice(0, i + 1).join('')
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
const start = process.hrtime.bigint()
|
|
384
|
+
progressive.forEach((value) =>
|
|
385
|
+
fireEvent.change(dateInput, { target: { value } })
|
|
386
|
+
)
|
|
387
|
+
const elapsedMs = Number(process.hrtime.bigint() - start) / 1e6
|
|
388
|
+
|
|
389
|
+
expect(elapsedMs).toBeLessThan(300)
|
|
390
|
+
})
|
|
391
|
+
})
|
|
392
|
+
|
|
64
393
|
describe('validation', () => {
|
|
394
|
+
it('should show a warning instead of an error when strictValidation is false', () => {
|
|
395
|
+
const onDateSelectMock = jest.fn()
|
|
396
|
+
const screen = render(
|
|
397
|
+
<CalendarWithValidation
|
|
398
|
+
calendar="gregory"
|
|
399
|
+
minDate="2024-01-01"
|
|
400
|
+
strictValidation={false}
|
|
401
|
+
onDateSelect={onDateSelectMock}
|
|
402
|
+
/>
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
const dateInputString = '2023-10-12'
|
|
406
|
+
const dateInput = within(
|
|
407
|
+
screen.getByTestId('dhis2-uicore-input')
|
|
408
|
+
).getByRole('textbox')
|
|
409
|
+
|
|
410
|
+
fireEvent.change(dateInput, { target: { value: dateInputString } })
|
|
411
|
+
fireEvent.blur(dateInput)
|
|
412
|
+
|
|
413
|
+
expect(
|
|
414
|
+
screen.getByText(
|
|
415
|
+
'Date 2023-10-12 is less than the minimum allowed date 2024-01-01.'
|
|
416
|
+
)
|
|
417
|
+
).toBeInTheDocument()
|
|
418
|
+
expect(onDateSelectMock).toHaveBeenCalledWith(
|
|
419
|
+
expect.objectContaining({
|
|
420
|
+
validation: expect.objectContaining({
|
|
421
|
+
warning: true,
|
|
422
|
+
valid: true,
|
|
423
|
+
}),
|
|
424
|
+
})
|
|
425
|
+
)
|
|
426
|
+
})
|
|
65
427
|
it('should validate minimum date', () => {
|
|
66
428
|
const onDateSelectMock = jest.fn()
|
|
67
429
|
const screen = render(
|
|
@@ -87,6 +449,90 @@ describe('Calendar Input', () => {
|
|
|
87
449
|
).toBeInTheDocument()
|
|
88
450
|
expect(onDateSelectMock).toHaveBeenCalledTimes(1)
|
|
89
451
|
})
|
|
452
|
+
|
|
453
|
+
// Regression test: minDate/maxDate used to reach validateDateString
|
|
454
|
+
// (as minDateString/maxDateString) but not useDatePicker itself, due to a
|
|
455
|
+
// key mismatch - so in strict mode the input rejected an out-of-range
|
|
456
|
+
// date while the widget still selected it. Warning mode is unaffected
|
|
457
|
+
// (see the test below) since useDatePicker only falls back to today when
|
|
458
|
+
// strictValidation produces an `error`.
|
|
459
|
+
it('calendar widget falls back to today for a date the input has flagged as below the minimum (strict mode)', async () => {
|
|
460
|
+
jest.useFakeTimers()
|
|
461
|
+
jest.setSystemTime(new Date('2024-10-22T09:05:00.000Z'))
|
|
462
|
+
|
|
463
|
+
const screen = render(
|
|
464
|
+
<CalendarWithValidation
|
|
465
|
+
calendar="gregory"
|
|
466
|
+
minDate="2024-01-01"
|
|
467
|
+
/>
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
const dateInput = within(
|
|
471
|
+
screen.getByTestId('dhis2-uicore-input')
|
|
472
|
+
).getByRole('textbox')
|
|
473
|
+
|
|
474
|
+
fireEvent.change(dateInput, { target: { value: '2020-01-15' } })
|
|
475
|
+
fireEvent.blur(dateInput)
|
|
476
|
+
|
|
477
|
+
expect(
|
|
478
|
+
screen.getByText(
|
|
479
|
+
'Date 2020-01-15 is less than the minimum allowed date 2024-01-01.'
|
|
480
|
+
)
|
|
481
|
+
).toBeInTheDocument()
|
|
482
|
+
|
|
483
|
+
fireEvent.focus(dateInput)
|
|
484
|
+
const calendar = await screen.findByTestId('calendar')
|
|
485
|
+
|
|
486
|
+
// expected: the widget falls back to today, like useDatePicker's own
|
|
487
|
+
// getInvalidDateResult does for a date it recognizes as invalid
|
|
488
|
+
expect(
|
|
489
|
+
within(calendar)
|
|
490
|
+
.getByTestId('2024-10-22')
|
|
491
|
+
.querySelector('button')
|
|
492
|
+
).toHaveClass('isToday')
|
|
493
|
+
|
|
494
|
+
jest.useRealTimers()
|
|
495
|
+
})
|
|
496
|
+
|
|
497
|
+
it('calendar widget correctly still selects an out-of-range date in warning mode', async () => {
|
|
498
|
+
jest.useFakeTimers()
|
|
499
|
+
jest.setSystemTime(new Date('2024-10-22T09:05:00.000Z'))
|
|
500
|
+
|
|
501
|
+
const screen = render(
|
|
502
|
+
<CalendarWithValidation
|
|
503
|
+
calendar="gregory"
|
|
504
|
+
minDate="2024-01-01"
|
|
505
|
+
strictValidation={false}
|
|
506
|
+
/>
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
const dateInput = within(
|
|
510
|
+
screen.getByTestId('dhis2-uicore-input')
|
|
511
|
+
).getByRole('textbox')
|
|
512
|
+
|
|
513
|
+
fireEvent.change(dateInput, { target: { value: '2020-01-15' } })
|
|
514
|
+
fireEvent.blur(dateInput)
|
|
515
|
+
|
|
516
|
+
expect(
|
|
517
|
+
screen.getByText(
|
|
518
|
+
'Date 2020-01-15 is less than the minimum allowed date 2024-01-01.'
|
|
519
|
+
)
|
|
520
|
+
).toBeInTheDocument()
|
|
521
|
+
|
|
522
|
+
fireEvent.focus(dateInput)
|
|
523
|
+
const calendar = await screen.findByTestId('calendar')
|
|
524
|
+
|
|
525
|
+
// warning mode means "flag it, but still let them pick it" - the
|
|
526
|
+
// widget showing the out-of-range date as selected is intentional
|
|
527
|
+
expect(
|
|
528
|
+
within(calendar)
|
|
529
|
+
.getByTestId('2020-01-15')
|
|
530
|
+
.querySelector('button')
|
|
531
|
+
).toHaveClass('isSelected')
|
|
532
|
+
|
|
533
|
+
jest.useRealTimers()
|
|
534
|
+
})
|
|
535
|
+
|
|
90
536
|
it('should validate maximum date', () => {
|
|
91
537
|
const { getByTestId, getByText } = render(
|
|
92
538
|
<CalendarWithValidation
|
|
@@ -109,8 +555,7 @@ describe('Calendar Input', () => {
|
|
|
109
555
|
)
|
|
110
556
|
).toBeInTheDocument()
|
|
111
557
|
})
|
|
112
|
-
|
|
113
|
-
it.skip('should validate date in ethiopic calendar', () => {
|
|
558
|
+
it('should validate date in ethiopic calendar', () => {
|
|
114
559
|
const onDateSelectMock = jest.fn()
|
|
115
560
|
const { getByTestId, getByText, queryByText } = render(
|
|
116
561
|
<CalendarWithValidation
|
|
@@ -155,7 +600,6 @@ describe('Calendar Input', () => {
|
|
|
155
600
|
getByText('Invalid date in specified calendar')
|
|
156
601
|
).toBeInTheDocument()
|
|
157
602
|
})
|
|
158
|
-
// ToDo: these scenarios seem to work but they timeout on CI sporadically - ticket: https://dhis2.atlassian.net/browse/LIBS-763
|
|
159
603
|
it('should validate date in nepali calendar', () => {
|
|
160
604
|
const onDateSelectMock = jest.fn()
|
|
161
605
|
const { getByTestId, getByText, queryByText } = render(
|
package/types/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as React from 'react'
|
|
2
|
-
import { useDatePicker } from '@dhis2/multi-calendar-dates'
|
|
2
|
+
import { useDatePicker, validateDateString } from '@dhis2/multi-calendar-dates'
|
|
3
3
|
import { InputFieldProps } from '@dhis2-ui/input'
|
|
4
4
|
|
|
5
5
|
export type CalendarDir = 'ltr' | 'rtl'
|
|
@@ -7,6 +7,25 @@ export type CalendarDir = 'ltr' | 'rtl'
|
|
|
7
7
|
type CalendarPickerParam = Parameters<typeof useDatePicker>[0]
|
|
8
8
|
type CalendarPickerOptions = CalendarPickerParam['options']
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* The `validation` object CalendarInput attaches to its `onDateSelect`
|
|
12
|
+
* payload. Not produced by the bare `Calendar` widget - CalendarInput
|
|
13
|
+
* computes it itself via `validateDateString` (whose `error`/`warning`/
|
|
14
|
+
* `valid` combination depends on the `strictValidation` prop), except
|
|
15
|
+
* when the date is cleared, in which case it's just `{ valid: true }`.
|
|
16
|
+
*/
|
|
17
|
+
export type CalendarInputValidation = ReturnType<typeof validateDateString>
|
|
18
|
+
|
|
19
|
+
export type CalendarInputOnDateSelectPayload =
|
|
20
|
+
| {
|
|
21
|
+
calendarDateString: string
|
|
22
|
+
validation: CalendarInputValidation
|
|
23
|
+
}
|
|
24
|
+
| {
|
|
25
|
+
calendarDateString: null
|
|
26
|
+
validation: { valid: true }
|
|
27
|
+
}
|
|
28
|
+
|
|
10
29
|
export interface CalendarProps {
|
|
11
30
|
/**
|
|
12
31
|
* the calendar to use such gregory, ethiopic, nepali - full supported list here: https://github.com/dhis2/multi-calendar-dates/blob/main/src/constants/calendars.ts
|
|
@@ -62,7 +81,14 @@ export interface CalendarProps {
|
|
|
62
81
|
export const Calendar: React.FC<CalendarProps>
|
|
63
82
|
|
|
64
83
|
export type CalendarInputProps = Omit<InputFieldProps, 'type' | 'value'> &
|
|
65
|
-
CalendarProps & {
|
|
84
|
+
Omit<CalendarProps, 'onDateSelect'> & {
|
|
85
|
+
/**
|
|
86
|
+
* Called with `{ calendarDateString: null, validation: { valid: true } }`
|
|
87
|
+
* when the date is cleared, otherwise with `{ calendarDateString: string, validation }`
|
|
88
|
+
* where `validation` reflects the result of validating against `minDate`/`maxDate`/
|
|
89
|
+
* `format` (see `CalendarInputValidation`), influenced by `strictValidation`.
|
|
90
|
+
*/
|
|
91
|
+
onDateSelect: (payload: CalendarInputOnDateSelectPayload) => void
|
|
66
92
|
/**
|
|
67
93
|
* Optional format for the date. Determines how the date is displayed
|
|
68
94
|
* or processed. If not provided it supports both formats
|