@dhis2-ui/calendar 10.16.2 → 10.16.3-alpha.1
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/package.json +10 -9
- package/src/__e2e__/calendar-input.e2e.stories.js +22 -0
- package/src/calendar/calendar-container.js +99 -0
- package/src/calendar/calendar-table-cell.js +97 -0
- package/src/calendar/calendar-table-days-header.js +40 -0
- package/src/calendar/calendar-table.js +74 -0
- package/src/calendar/calendar.js +104 -0
- package/src/calendar/navigation-container.js +295 -0
- package/src/calendar-input/__tests__/calendar-input.test.js +344 -0
- package/src/calendar-input/calendar-input.js +262 -0
- package/src/features/supports_calendar_clear_button/supports_calendar_clear_button.js +64 -0
- package/src/features/supports_calendar_clear_button.feature +23 -0
- package/src/features/supports_ethiopic_calendar/supports_ethiopic_calendar.js +61 -0
- package/src/features/supports_ethiopic_calendar.feature +20 -0
- package/src/features/supports_gregorian_calendar/supports_gregorian_calendar.js +62 -0
- package/src/features/supports_gregorian_calendar.feature +19 -0
- package/src/features/supports_islamic_calendar/supports_islamic_calendar.js +17 -0
- package/src/features/supports_islamic_calendar.feature +5 -0
- package/src/features/supports_nepali_calendar/supports_nepali_calendar.js +60 -0
- package/src/features/supports_nepali_calendar.feature +19 -0
- package/src/index.js +2 -0
- package/src/locales/en/translations.json +6 -0
- package/src/locales/index.js +16 -0
- package/src/stories/calendar-input.prod.stories.js +255 -0
- package/src/stories/calendar-story-wrapper.js +161 -0
- package/src/stories/calendar.prod.stories.js +91 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { colors, spacers } from '@dhis2/ui-constants'
|
|
2
|
+
import { IconChevronLeft16, IconChevronRight16 } from '@dhis2/ui-icons'
|
|
3
|
+
import PropTypes from 'prop-types'
|
|
4
|
+
import React from 'react'
|
|
5
|
+
import i18n from '../locales/index.js'
|
|
6
|
+
|
|
7
|
+
const wrapperBorderColor = colors.grey300
|
|
8
|
+
const headerBackground = colors.grey100
|
|
9
|
+
|
|
10
|
+
export const NavigationContainer = ({
|
|
11
|
+
languageDirection,
|
|
12
|
+
currMonth,
|
|
13
|
+
currYear,
|
|
14
|
+
nextMonth,
|
|
15
|
+
nextYear,
|
|
16
|
+
prevMonth,
|
|
17
|
+
prevYear,
|
|
18
|
+
navigateToYear,
|
|
19
|
+
navigateToMonth,
|
|
20
|
+
months,
|
|
21
|
+
years,
|
|
22
|
+
}) => {
|
|
23
|
+
const PreviousIcon =
|
|
24
|
+
languageDirection === 'ltr' ? IconChevronLeft16 : IconChevronRight16
|
|
25
|
+
const NextIcon =
|
|
26
|
+
languageDirection === 'ltr' ? IconChevronRight16 : IconChevronLeft16
|
|
27
|
+
|
|
28
|
+
const handleYearChange = (e) => {
|
|
29
|
+
const targetYear = parseInt(e.target.value)
|
|
30
|
+
navigateToYear(targetYear)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const handleMonthChange = (e) => {
|
|
34
|
+
const selectedMonth = months.find(
|
|
35
|
+
(month) => month.label === e.target.value
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
if (selectedMonth) {
|
|
39
|
+
navigateToMonth(selectedMonth.value)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<>
|
|
45
|
+
<div className="navigation-container">
|
|
46
|
+
<div className="month">
|
|
47
|
+
<div className="prev">
|
|
48
|
+
<button
|
|
49
|
+
onClick={prevMonth.navigateTo}
|
|
50
|
+
name="previous-month"
|
|
51
|
+
data-test="calendar-previous-month"
|
|
52
|
+
aria-label={`${i18n.t(`Go to ${prevMonth.label}`)}`}
|
|
53
|
+
type="button"
|
|
54
|
+
>
|
|
55
|
+
<PreviousIcon />
|
|
56
|
+
</button>
|
|
57
|
+
</div>
|
|
58
|
+
<div className="monthList">
|
|
59
|
+
<select
|
|
60
|
+
value={currMonth.label}
|
|
61
|
+
onChange={handleMonthChange}
|
|
62
|
+
className="month-select"
|
|
63
|
+
data-test="calendar-month-select"
|
|
64
|
+
>
|
|
65
|
+
{months.map((month) => (
|
|
66
|
+
<option key={month.value} value={month.label}>
|
|
67
|
+
{month.label}
|
|
68
|
+
</option>
|
|
69
|
+
))}
|
|
70
|
+
</select>
|
|
71
|
+
<svg
|
|
72
|
+
width="16"
|
|
73
|
+
height="16"
|
|
74
|
+
viewBox="0 0 16 16"
|
|
75
|
+
fill="none"
|
|
76
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
77
|
+
>
|
|
78
|
+
<path
|
|
79
|
+
d="M10.1465 6.85363L10.8536 6.14652L8.00004 3.29297L5.14648 6.14652L5.85359 6.85363L8.00004 4.70718L10.1465 6.85363ZM5.85367 9.1466L5.14656 9.8537L8.00011 12.7073L10.8537 9.8537L10.1466 9.1466L8.00011 11.293L5.85367 9.1466Z"
|
|
80
|
+
fill={colors.grey700}
|
|
81
|
+
/>
|
|
82
|
+
</svg>
|
|
83
|
+
</div>
|
|
84
|
+
<div className="next">
|
|
85
|
+
<button
|
|
86
|
+
onClick={nextMonth.navigateTo}
|
|
87
|
+
data-test="calendar-next-month"
|
|
88
|
+
name="next-month"
|
|
89
|
+
aria-label={`${i18n.t(`Go to ${nextMonth.label}`)}`}
|
|
90
|
+
type="button"
|
|
91
|
+
>
|
|
92
|
+
<NextIcon />
|
|
93
|
+
</button>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
<div className="year">
|
|
97
|
+
<div className="prev">
|
|
98
|
+
<button
|
|
99
|
+
onClick={prevYear.navigateTo}
|
|
100
|
+
name="previous-year"
|
|
101
|
+
aria-label={`${i18n.t('Go to previous year')}`}
|
|
102
|
+
type="button"
|
|
103
|
+
>
|
|
104
|
+
<PreviousIcon />
|
|
105
|
+
</button>
|
|
106
|
+
</div>
|
|
107
|
+
<div className="yearList">
|
|
108
|
+
<select
|
|
109
|
+
value={currYear.value}
|
|
110
|
+
onChange={handleYearChange}
|
|
111
|
+
className="year-select"
|
|
112
|
+
data-test="calendar-year-select"
|
|
113
|
+
>
|
|
114
|
+
{years.map((year) => (
|
|
115
|
+
<option key={year.value} value={year.value}>
|
|
116
|
+
{/* ToDo: this is a workaround for Ethiopic years showing the era
|
|
117
|
+
The workaround is needed but should be done in multi-calendar lib */}
|
|
118
|
+
{year.label?.replace(/ERA\d/, '')}
|
|
119
|
+
</option>
|
|
120
|
+
))}
|
|
121
|
+
</select>
|
|
122
|
+
<svg
|
|
123
|
+
width="16"
|
|
124
|
+
height="16"
|
|
125
|
+
viewBox="0 0 16 16"
|
|
126
|
+
fill="none"
|
|
127
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
128
|
+
>
|
|
129
|
+
<path
|
|
130
|
+
d="M10.1465 6.85363L10.8536 6.14652L8.00004 3.29297L5.14648 6.14652L5.85359 6.85363L8.00004 4.70718L10.1465 6.85363ZM5.85367 9.1466L5.14656 9.8537L8.00011 12.7073L10.8537 9.8537L10.1466 9.1466L8.00011 11.293L5.85367 9.1466Z"
|
|
131
|
+
fill={colors.grey700}
|
|
132
|
+
/>
|
|
133
|
+
</svg>
|
|
134
|
+
</div>
|
|
135
|
+
<div className="next">
|
|
136
|
+
<button
|
|
137
|
+
onClick={nextYear.navigateTo}
|
|
138
|
+
name="next-year"
|
|
139
|
+
aria-label={`${i18n.t('Go to next year')}`}
|
|
140
|
+
type="button"
|
|
141
|
+
>
|
|
142
|
+
<NextIcon />
|
|
143
|
+
</button>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
<style jsx>{`
|
|
148
|
+
.navigation-container {
|
|
149
|
+
display: flex;
|
|
150
|
+
justify-content: space-between;
|
|
151
|
+
gap: ${spacers.dp4};
|
|
152
|
+
padding: ${spacers.dp4};
|
|
153
|
+
border-bottom: 1px solid ${wrapperBorderColor};
|
|
154
|
+
background-color: ${headerBackground};
|
|
155
|
+
font-size: 1em;
|
|
156
|
+
width: 100%;
|
|
157
|
+
}
|
|
158
|
+
.month,
|
|
159
|
+
.year {
|
|
160
|
+
display: flex;
|
|
161
|
+
align-items: center;
|
|
162
|
+
justify-content: space-between;
|
|
163
|
+
border: 1px solid ${colors.grey300};
|
|
164
|
+
border-radius: 3px;
|
|
165
|
+
background: ${colors.white};
|
|
166
|
+
}
|
|
167
|
+
.month {
|
|
168
|
+
flex-grow: 1;
|
|
169
|
+
}
|
|
170
|
+
.prev {
|
|
171
|
+
border-inline-end: 1px solid ${colors.grey300};
|
|
172
|
+
}
|
|
173
|
+
.next {
|
|
174
|
+
border-inline-start: 1px solid ${colors.grey300};
|
|
175
|
+
}
|
|
176
|
+
.prev,
|
|
177
|
+
.next,
|
|
178
|
+
.monthList,
|
|
179
|
+
.yearList {
|
|
180
|
+
display: flex;
|
|
181
|
+
align-items: center;
|
|
182
|
+
justify-content: center;
|
|
183
|
+
}
|
|
184
|
+
.prev,
|
|
185
|
+
.next {
|
|
186
|
+
width: 20px;
|
|
187
|
+
flex-shrink: 0;
|
|
188
|
+
}
|
|
189
|
+
.monthList,
|
|
190
|
+
.yearList {
|
|
191
|
+
flex: 0 1 auto;
|
|
192
|
+
overflow: hidden;
|
|
193
|
+
position: relative;
|
|
194
|
+
}
|
|
195
|
+
.monthList {
|
|
196
|
+
flex-grow: 1;
|
|
197
|
+
width: 100%;
|
|
198
|
+
}
|
|
199
|
+
.monthList svg,
|
|
200
|
+
.yearList svg {
|
|
201
|
+
position: absolute;
|
|
202
|
+
inset-inline-end: 0px;
|
|
203
|
+
pointer-events: none;
|
|
204
|
+
}
|
|
205
|
+
button {
|
|
206
|
+
background: none;
|
|
207
|
+
border: 0;
|
|
208
|
+
padding: ${spacers.dp4} 2px;
|
|
209
|
+
height: 24px;
|
|
210
|
+
width: 20px;
|
|
211
|
+
color: ${colors.grey700};
|
|
212
|
+
display: flex;
|
|
213
|
+
align-items: center;
|
|
214
|
+
justify-content: center;
|
|
215
|
+
}
|
|
216
|
+
button:hover {
|
|
217
|
+
background-color: ${colors.grey200};
|
|
218
|
+
color: ${colors.grey900};
|
|
219
|
+
cursor: pointer;
|
|
220
|
+
}
|
|
221
|
+
.month-select,
|
|
222
|
+
.year-select {
|
|
223
|
+
padding-inline-start: 4px;
|
|
224
|
+
padding-inline-end: 16px;
|
|
225
|
+
height: 24px;
|
|
226
|
+
white-space: nowrap;
|
|
227
|
+
overflow: hidden;
|
|
228
|
+
text-align: start;
|
|
229
|
+
width: 100%;
|
|
230
|
+
max-width: 100%;
|
|
231
|
+
border-radius: 0px;
|
|
232
|
+
border: 0;
|
|
233
|
+
color: ${colors.grey800};
|
|
234
|
+
background: none;
|
|
235
|
+
appearance: none;
|
|
236
|
+
}
|
|
237
|
+
.month-select:hover,
|
|
238
|
+
.year-select:hover {
|
|
239
|
+
background: ${colors.grey200};
|
|
240
|
+
cursor: pointer;
|
|
241
|
+
}
|
|
242
|
+
.month-select:focus,
|
|
243
|
+
.month-select-active,
|
|
244
|
+
.year-select:focus,
|
|
245
|
+
.year-select-active {
|
|
246
|
+
background: ${colors.grey200};
|
|
247
|
+
outline-color: ${colors.grey700};
|
|
248
|
+
}
|
|
249
|
+
`}</style>
|
|
250
|
+
</>
|
|
251
|
+
)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export const NavigationContainerProps = {
|
|
255
|
+
currMonth: PropTypes.shape({
|
|
256
|
+
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
|
257
|
+
}),
|
|
258
|
+
currYear: PropTypes.shape({
|
|
259
|
+
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
|
260
|
+
value: PropTypes.number,
|
|
261
|
+
}),
|
|
262
|
+
languageDirection: PropTypes.oneOf(['ltr', 'rtl']),
|
|
263
|
+
months: PropTypes.arrayOf(
|
|
264
|
+
PropTypes.shape({
|
|
265
|
+
label: PropTypes.string.isRequired,
|
|
266
|
+
value: PropTypes.number.isRequired,
|
|
267
|
+
})
|
|
268
|
+
),
|
|
269
|
+
navigateToMonth: PropTypes.func,
|
|
270
|
+
navigateToYear: PropTypes.func,
|
|
271
|
+
nextMonth: PropTypes.shape({
|
|
272
|
+
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
|
273
|
+
navigateTo: PropTypes.func,
|
|
274
|
+
}),
|
|
275
|
+
nextYear: PropTypes.shape({
|
|
276
|
+
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
|
277
|
+
navigateTo: PropTypes.func,
|
|
278
|
+
}),
|
|
279
|
+
prevMonth: PropTypes.shape({
|
|
280
|
+
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
|
281
|
+
navigateTo: PropTypes.func,
|
|
282
|
+
}),
|
|
283
|
+
prevYear: PropTypes.shape({
|
|
284
|
+
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
|
285
|
+
navigateTo: PropTypes.func,
|
|
286
|
+
}),
|
|
287
|
+
years: PropTypes.arrayOf(
|
|
288
|
+
PropTypes.shape({
|
|
289
|
+
label: PropTypes.string.isRequired,
|
|
290
|
+
value: PropTypes.number.isRequired,
|
|
291
|
+
})
|
|
292
|
+
),
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
NavigationContainer.propTypes = NavigationContainerProps
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { Button } from '@dhis2-ui/button'
|
|
2
|
+
import { fireEvent, render, waitFor, within } from '@testing-library/react'
|
|
3
|
+
import React, { useState } from 'react'
|
|
4
|
+
import { Field, Form } from 'react-final-form'
|
|
5
|
+
import { CalendarInput } from '../calendar-input.js'
|
|
6
|
+
|
|
7
|
+
describe('Calendar Input', () => {
|
|
8
|
+
it('allow selection of a date through the calendar widget', async () => {
|
|
9
|
+
jest.useFakeTimers()
|
|
10
|
+
jest.setSystemTime(new Date('2024-10-22T09:05:00.000Z'))
|
|
11
|
+
|
|
12
|
+
const onDateSelectMock = jest.fn()
|
|
13
|
+
const screen = render(
|
|
14
|
+
<CalendarInput calendar="gregory" onDateSelect={onDateSelectMock} />
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
const dateInput = within(
|
|
18
|
+
screen.getByTestId('dhis2-uicore-input')
|
|
19
|
+
).getByRole('textbox')
|
|
20
|
+
|
|
21
|
+
fireEvent.focus(dateInput)
|
|
22
|
+
|
|
23
|
+
const calendar = await screen.findByTestId('calendar')
|
|
24
|
+
expect(calendar).toBeInTheDocument()
|
|
25
|
+
|
|
26
|
+
const todayString = '2024-10-22'
|
|
27
|
+
const today = within(calendar).getByTestId(todayString)
|
|
28
|
+
|
|
29
|
+
fireEvent.click(today)
|
|
30
|
+
|
|
31
|
+
await waitFor(() => {
|
|
32
|
+
expect(calendar).not.toBeInTheDocument()
|
|
33
|
+
})
|
|
34
|
+
expect(onDateSelectMock).toHaveBeenCalledWith(
|
|
35
|
+
expect.objectContaining({
|
|
36
|
+
calendarDateString: todayString,
|
|
37
|
+
})
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
jest.useRealTimers()
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('allow selection of a date through the input', async () => {
|
|
44
|
+
const onDateSelectMock = jest.fn()
|
|
45
|
+
const screen = render(
|
|
46
|
+
<CalendarInput calendar="gregory" onDateSelect={onDateSelectMock} />
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
const dateInputString = '2024/10/12'
|
|
50
|
+
const dateInput = within(
|
|
51
|
+
screen.getByTestId('dhis2-uicore-input')
|
|
52
|
+
).getByRole('textbox')
|
|
53
|
+
|
|
54
|
+
fireEvent.change(dateInput, { target: { value: dateInputString } })
|
|
55
|
+
fireEvent.blur(dateInput)
|
|
56
|
+
|
|
57
|
+
expect(onDateSelectMock).toHaveBeenCalledWith(
|
|
58
|
+
expect.objectContaining({
|
|
59
|
+
calendarDateString: dateInputString,
|
|
60
|
+
})
|
|
61
|
+
)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
describe('validation', () => {
|
|
65
|
+
it('should validate minimum date', () => {
|
|
66
|
+
const onDateSelectMock = jest.fn()
|
|
67
|
+
const screen = render(
|
|
68
|
+
<CalendarWithValidation
|
|
69
|
+
calendar="gregory"
|
|
70
|
+
minDate="2024-01-01"
|
|
71
|
+
onDateSelect={onDateSelectMock}
|
|
72
|
+
/>
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
const dateInputString = '2023-10-12'
|
|
76
|
+
const dateInput = within(
|
|
77
|
+
screen.getByTestId('dhis2-uicore-input')
|
|
78
|
+
).getByRole('textbox')
|
|
79
|
+
|
|
80
|
+
fireEvent.change(dateInput, { target: { value: dateInputString } })
|
|
81
|
+
fireEvent.blur(dateInput)
|
|
82
|
+
|
|
83
|
+
expect(
|
|
84
|
+
screen.getByText(
|
|
85
|
+
'Date 2023-10-12 is less than the minimum allowed date 2024-01-01.'
|
|
86
|
+
)
|
|
87
|
+
).toBeInTheDocument()
|
|
88
|
+
expect(onDateSelectMock).toHaveBeenCalledTimes(1)
|
|
89
|
+
})
|
|
90
|
+
it('should validate maximum date', () => {
|
|
91
|
+
const { getByTestId, getByText } = render(
|
|
92
|
+
<CalendarWithValidation
|
|
93
|
+
calendar="gregory"
|
|
94
|
+
maxDate="2024-01-01"
|
|
95
|
+
/>
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
const dateInputString = '2024-10-12'
|
|
99
|
+
const dateInput = within(
|
|
100
|
+
getByTestId('dhis2-uicore-input')
|
|
101
|
+
).getByRole('textbox')
|
|
102
|
+
|
|
103
|
+
fireEvent.change(dateInput, { target: { value: dateInputString } })
|
|
104
|
+
fireEvent.blur(dateInput)
|
|
105
|
+
|
|
106
|
+
expect(
|
|
107
|
+
getByText(
|
|
108
|
+
'Date 2024-10-12 is greater than the maximum allowed date 2024-01-01.'
|
|
109
|
+
)
|
|
110
|
+
).toBeInTheDocument()
|
|
111
|
+
})
|
|
112
|
+
// Temporarily disabled
|
|
113
|
+
it.skip('should validate date in ethiopic calendar', () => {
|
|
114
|
+
const onDateSelectMock = jest.fn()
|
|
115
|
+
const { getByTestId, getByText, queryByText } = render(
|
|
116
|
+
<CalendarWithValidation
|
|
117
|
+
calendar="ethiopian"
|
|
118
|
+
minDate="2018-13-04"
|
|
119
|
+
onDateSelect={onDateSelectMock}
|
|
120
|
+
/>
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
let dateInputString = '2018-13-02'
|
|
124
|
+
const dateInput = within(
|
|
125
|
+
getByTestId('dhis2-uicore-input')
|
|
126
|
+
).getByRole('textbox')
|
|
127
|
+
|
|
128
|
+
fireEvent.change(dateInput, { target: { value: dateInputString } })
|
|
129
|
+
fireEvent.blur(dateInput)
|
|
130
|
+
|
|
131
|
+
expect(
|
|
132
|
+
getByTestId('dhis2-uiwidgets-calendar-inputfield-validation')
|
|
133
|
+
).toBeInTheDocument()
|
|
134
|
+
expect(
|
|
135
|
+
getByText(
|
|
136
|
+
'Date 2018-13-02 is less than the minimum allowed date 2018-13-04.'
|
|
137
|
+
)
|
|
138
|
+
).toBeInTheDocument()
|
|
139
|
+
|
|
140
|
+
dateInputString = '2018-13-05'
|
|
141
|
+
fireEvent.change(dateInput, { target: { value: dateInputString } })
|
|
142
|
+
fireEvent.blur(dateInput)
|
|
143
|
+
|
|
144
|
+
expect(
|
|
145
|
+
queryByText(
|
|
146
|
+
'Date 2018-13-04 is less than the minimum allowed date 2018-13-05.'
|
|
147
|
+
)
|
|
148
|
+
).not.toBeInTheDocument()
|
|
149
|
+
|
|
150
|
+
dateInputString = '2018-13-07'
|
|
151
|
+
fireEvent.change(dateInput, { target: { value: dateInputString } })
|
|
152
|
+
fireEvent.blur(dateInput)
|
|
153
|
+
|
|
154
|
+
expect(
|
|
155
|
+
getByText('Invalid date in specified calendar')
|
|
156
|
+
).toBeInTheDocument()
|
|
157
|
+
})
|
|
158
|
+
// ToDo: these scenarios seem to work but they timeout on CI sporadically - ticket: https://dhis2.atlassian.net/browse/LIBS-763
|
|
159
|
+
it('should validate date in nepali calendar', () => {
|
|
160
|
+
const onDateSelectMock = jest.fn()
|
|
161
|
+
const { getByTestId, getByText, queryByText } = render(
|
|
162
|
+
<CalendarWithValidation
|
|
163
|
+
calendar="nepali"
|
|
164
|
+
maxDate="2080-05-30"
|
|
165
|
+
onDateSelect={onDateSelectMock}
|
|
166
|
+
/>
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
let dateInputString = '2080-06-01'
|
|
170
|
+
const dateInput = within(
|
|
171
|
+
getByTestId('dhis2-uicore-input')
|
|
172
|
+
).getByRole('textbox')
|
|
173
|
+
|
|
174
|
+
fireEvent.change(dateInput, { target: { value: dateInputString } })
|
|
175
|
+
fireEvent.blur(dateInput)
|
|
176
|
+
|
|
177
|
+
expect(
|
|
178
|
+
getByText(
|
|
179
|
+
'Date 2080-06-01 is greater than the maximum allowed date 2080-05-30.'
|
|
180
|
+
)
|
|
181
|
+
).toBeInTheDocument()
|
|
182
|
+
|
|
183
|
+
dateInputString = '2080-04-32'
|
|
184
|
+
fireEvent.change(dateInput, { target: { value: dateInputString } })
|
|
185
|
+
fireEvent.blur(dateInput)
|
|
186
|
+
|
|
187
|
+
expect(
|
|
188
|
+
queryByText(/greater than the maximum allowed date/)
|
|
189
|
+
).not.toBeInTheDocument()
|
|
190
|
+
|
|
191
|
+
dateInputString = '2080-01-32'
|
|
192
|
+
fireEvent.change(dateInput, { target: { value: dateInputString } })
|
|
193
|
+
fireEvent.blur(dateInput)
|
|
194
|
+
|
|
195
|
+
expect(
|
|
196
|
+
getByText('Invalid date in specified calendar')
|
|
197
|
+
).toBeInTheDocument()
|
|
198
|
+
})
|
|
199
|
+
it('should validate from date picker', () => {
|
|
200
|
+
jest.useFakeTimers()
|
|
201
|
+
jest.setSystemTime(new Date('2024-10-22T09:05:00.000Z'))
|
|
202
|
+
|
|
203
|
+
const onDateSelectMock = jest.fn()
|
|
204
|
+
const { queryByText, getByText, getByTestId } = render(
|
|
205
|
+
<CalendarWithValidation
|
|
206
|
+
calendar="gregory"
|
|
207
|
+
minDate="2024-02-16"
|
|
208
|
+
onDateSelect={onDateSelectMock}
|
|
209
|
+
/>
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
const dateInput = within(
|
|
213
|
+
getByTestId('dhis2-uicore-input')
|
|
214
|
+
).getByRole('textbox')
|
|
215
|
+
|
|
216
|
+
fireEvent.focusIn(dateInput)
|
|
217
|
+
fireEvent.click(getByText('17'))
|
|
218
|
+
|
|
219
|
+
expect(queryByText('17')).not.toBeInTheDocument()
|
|
220
|
+
|
|
221
|
+
// Checking fix for Bug where callback used to be called twice - first with undefined
|
|
222
|
+
expect(onDateSelectMock).toHaveBeenCalledTimes(1)
|
|
223
|
+
expect(onDateSelectMock).toHaveBeenCalledWith({
|
|
224
|
+
calendarDateString: '2024-10-17',
|
|
225
|
+
validation: { error: false, valid: true, warning: false },
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
jest.useRealTimers()
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it('should validate with Clear', () => {
|
|
232
|
+
const onDateSelectMock = jest.fn()
|
|
233
|
+
const { queryByText, getByText, getByTestId } = render(
|
|
234
|
+
<CalendarWithValidation
|
|
235
|
+
calendar="gregory"
|
|
236
|
+
minDate="2024-02-16"
|
|
237
|
+
onDateSelect={onDateSelectMock}
|
|
238
|
+
clearable
|
|
239
|
+
/>
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
const dateInputString = '2023-10-12'
|
|
243
|
+
const dateInput = within(
|
|
244
|
+
getByTestId('dhis2-uicore-input')
|
|
245
|
+
).getByRole('textbox')
|
|
246
|
+
|
|
247
|
+
fireEvent.change(dateInput, { target: { value: dateInputString } })
|
|
248
|
+
fireEvent.blur(dateInput)
|
|
249
|
+
|
|
250
|
+
expect(
|
|
251
|
+
getByTestId('dhis2-uiwidgets-calendar-inputfield-validation')
|
|
252
|
+
).toBeInTheDocument()
|
|
253
|
+
|
|
254
|
+
fireEvent.click(getByText('Clear'))
|
|
255
|
+
expect(queryByText('17')).not.toBeInTheDocument()
|
|
256
|
+
|
|
257
|
+
expect(onDateSelectMock).toHaveBeenLastCalledWith({
|
|
258
|
+
calendarDateString: null,
|
|
259
|
+
validation: { valid: true },
|
|
260
|
+
})
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
it('should validate when Clearing manually (i.e. deleting text not using clear button)', () => {
|
|
264
|
+
const onDateSelectMock = jest.fn()
|
|
265
|
+
const { getByTestId } = render(
|
|
266
|
+
<CalendarWithValidation
|
|
267
|
+
calendar="gregory"
|
|
268
|
+
minDate="2024-02-16"
|
|
269
|
+
onDateSelect={onDateSelectMock}
|
|
270
|
+
clearable
|
|
271
|
+
/>
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
const dateInputString = '2023-10-12'
|
|
275
|
+
const dateInput = within(
|
|
276
|
+
getByTestId('dhis2-uicore-input')
|
|
277
|
+
).getByRole('textbox')
|
|
278
|
+
|
|
279
|
+
fireEvent.change(dateInput, { target: { value: dateInputString } })
|
|
280
|
+
fireEvent.blur(dateInput)
|
|
281
|
+
|
|
282
|
+
expect(
|
|
283
|
+
getByTestId('dhis2-uiwidgets-calendar-inputfield-validation')
|
|
284
|
+
).toBeInTheDocument()
|
|
285
|
+
|
|
286
|
+
fireEvent.change(dateInput, { target: { value: '' } })
|
|
287
|
+
fireEvent.blur(dateInput)
|
|
288
|
+
|
|
289
|
+
expect(onDateSelectMock).toHaveBeenCalledWith({
|
|
290
|
+
calendarDateString: null,
|
|
291
|
+
validation: { valid: true },
|
|
292
|
+
})
|
|
293
|
+
})
|
|
294
|
+
})
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
const CalendarWithValidation = (propsFromParent) => {
|
|
298
|
+
const [date, setDate] = useState()
|
|
299
|
+
|
|
300
|
+
const [validation, setValidation] = useState({})
|
|
301
|
+
|
|
302
|
+
const errored = () => {
|
|
303
|
+
if (validation?.error) {
|
|
304
|
+
return { calendar: validation.validationText }
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return (
|
|
309
|
+
<Form onSubmit={() => {}} validate={errored}>
|
|
310
|
+
{({ handleSubmit, invalid }) => {
|
|
311
|
+
return (
|
|
312
|
+
<form>
|
|
313
|
+
<Field name="calendar">
|
|
314
|
+
{(props) => (
|
|
315
|
+
<CalendarInput
|
|
316
|
+
{...props}
|
|
317
|
+
date={date}
|
|
318
|
+
label="Enter a date"
|
|
319
|
+
editable
|
|
320
|
+
calendar="gregory"
|
|
321
|
+
{...validation}
|
|
322
|
+
{...propsFromParent}
|
|
323
|
+
onDateSelect={(date) => {
|
|
324
|
+
setDate(date?.calendarDateString)
|
|
325
|
+
setValidation(date?.validation)
|
|
326
|
+
propsFromParent.onDateSelect?.(date)
|
|
327
|
+
}}
|
|
328
|
+
/>
|
|
329
|
+
)}
|
|
330
|
+
</Field>
|
|
331
|
+
|
|
332
|
+
<Button
|
|
333
|
+
type="submit"
|
|
334
|
+
disabled={invalid}
|
|
335
|
+
onClick={handleSubmit}
|
|
336
|
+
>
|
|
337
|
+
Submit
|
|
338
|
+
</Button>
|
|
339
|
+
</form>
|
|
340
|
+
)
|
|
341
|
+
}}
|
|
342
|
+
</Form>
|
|
343
|
+
)
|
|
344
|
+
}
|