diff --git a/components/calendar/src/calendar-input/__tests__/calendar-input.test.js b/components/calendar/src/calendar-input/__tests__/calendar-input.test.js
index 75be8ec2fe..d0507ae741 100644
--- a/components/calendar/src/calendar-input/__tests__/calendar-input.test.js
+++ b/components/calendar/src/calendar-input/__tests__/calendar-input.test.js
@@ -1,8 +1,17 @@
+import { Button } from '@dhis2-ui/button'
import { fireEvent, render, waitFor, within } from '@testing-library/react'
-import React from 'react'
+import userEvent from '@testing-library/user-event'
+import React, { useState } from 'react'
+import { Field, Form } from 'react-final-form'
import { CalendarInput } from '../calendar-input.js'
describe('Calendar Input', () => {
+ beforeEach(() => {
+ jest.useFakeTimers()
+ jest.setSystemTime(new Date('2024-10-22T09:05:00.000Z'))
+ })
+ afterEach(jest.useRealTimers)
+
it('allow selection of a date through the calendar widget', async () => {
const onDateSelectMock = jest.fn()
const screen = render(
@@ -16,7 +25,7 @@ describe('Calendar Input', () => {
const calendar = await screen.findByTestId('calendar')
expect(calendar).toBeInTheDocument()
- const todayString = new Date().toISOString().slice(0, -14)
+ const todayString = '2024-10-22'
const today = within(calendar).getByTestId(todayString)
fireEvent.click(today)
@@ -51,4 +60,284 @@ describe('Calendar Input', () => {
})
)
})
+
+ describe('validation', () => {
+ it('should validate minimum date', async () => {
+ const onDateSelectMock = jest.fn()
+ const screen = render(
+
+ )
+
+ const dateInputString = '2023-10-12'
+ const dateInput = within(
+ screen.getByTestId('dhis2-uicore-input')
+ ).getByRole('textbox')
+
+ userEvent.clear(dateInput)
+ userEvent.type(dateInput, dateInputString)
+ userEvent.tab()
+
+ expect(
+ await screen.findByText(
+ 'Date 2023-10-12 is less than the minimum allowed date 2024-01-01.'
+ )
+ )
+ expect(onDateSelectMock).toHaveBeenCalledTimes(1)
+ })
+ it('should validate maximum date', async () => {
+ const { getByTestId, findByText } = render(
+
+ )
+
+ const dateInputString = '2024-10-12'
+ const dateInput = within(
+ getByTestId('dhis2-uicore-input')
+ ).getByRole('textbox')
+
+ userEvent.clear(dateInput)
+ userEvent.type(dateInput, dateInputString)
+ userEvent.tab()
+
+ expect(
+ await findByText(
+ 'Date 2024-10-12 is greater than the maximum allowed date 2024-01-01.'
+ )
+ )
+ })
+ it('should validate date in ethiopic calendar', async () => {
+ const onDateSelectMock = jest.fn()
+ const { getByTestId, findByText, queryByText } = render(
+
+ )
+
+ let dateInputString = '2018-13-02'
+ const dateInput = within(
+ getByTestId('dhis2-uicore-input')
+ ).getByRole('textbox')
+
+ userEvent.clear(dateInput)
+ userEvent.type(dateInput, dateInputString)
+ userEvent.tab()
+
+ expect(
+ await findByText(
+ 'Date 2018-13-02 is less than the minimum allowed date 2018-13-04.'
+ )
+ )
+
+ dateInputString = '2018-13-05'
+ userEvent.clear(dateInput)
+ userEvent.type(dateInput, dateInputString)
+ userEvent.tab()
+
+ expect(
+ queryByText(
+ 'Date 2018-13-04 is less than the minimum allowed date 2018-13-05.'
+ )
+ ).not.toBeInTheDocument()
+
+ dateInputString = '2018-13-07'
+ userEvent.clear(dateInput)
+ userEvent.type(dateInput, dateInputString)
+ userEvent.tab()
+
+ expect(
+ await findByText('Invalid date in specified calendar')
+ ).toBeInTheDocument()
+ })
+ it('should validate date in nepali calendar', async () => {
+ const onDateSelectMock = jest.fn()
+ const { getByTestId, findByText, queryByText } = render(
+
+ )
+
+ let dateInputString = '2080-06-01'
+ const dateInput = within(
+ getByTestId('dhis2-uicore-input')
+ ).getByRole('textbox')
+
+ userEvent.clear(dateInput)
+ userEvent.type(dateInput, dateInputString)
+ userEvent.tab()
+
+ expect(
+ await findByText(
+ 'Date 2080-06-01 is greater than the maximum allowed date 2080-05-30.'
+ )
+ )
+
+ dateInputString = '2080-04-32'
+ userEvent.clear(dateInput)
+ userEvent.type(dateInput, dateInputString)
+ userEvent.tab()
+
+ expect(
+ queryByText(/greater than the maximum allowed date/)
+ ).not.toBeInTheDocument()
+
+ dateInputString = '2080-01-32'
+ userEvent.clear(dateInput)
+ userEvent.type(dateInput, dateInputString)
+ userEvent.tab()
+
+ expect(
+ await findByText('Invalid date in specified calendar')
+ ).toBeInTheDocument()
+ })
+ it('should validate from date picker', async () => {
+ const onDateSelectMock = jest.fn()
+ const { queryByText, getByText, getByTestId } = render(
+
+ )
+
+ const dateInput = within(
+ getByTestId('dhis2-uicore-input')
+ ).getByRole('textbox')
+
+ await userEvent.click(dateInput)
+ await userEvent.click(getByText('17'))
+ expect(queryByText('17')).not.toBeInTheDocument()
+
+ // Bug where callback used to be called first with undefined
+ expect(onDateSelectMock).toHaveBeenCalledTimes(1)
+ expect(onDateSelectMock).toHaveBeenCalledWith({
+ calendarDateString: '2024-10-17',
+ validation: { error: false, valid: true, warning: false },
+ })
+ })
+
+ it('should validate with Clear', async () => {
+ const onDateSelectMock = jest.fn()
+ const { queryByText, getByText, getByTestId } = render(
+
+ )
+
+ const dateInputString = '2023-10-12'
+ const dateInput = within(
+ getByTestId('dhis2-uicore-input')
+ ).getByRole('textbox')
+
+ userEvent.clear(dateInput)
+ userEvent.type(dateInput, dateInputString)
+ userEvent.tab()
+
+ expect(
+ getByTestId('dhis2-uiwidgets-calendar-inputfield-validation')
+ ).toBeInTheDocument()
+
+ await userEvent.click(getByText('Clear'))
+ expect(queryByText('17')).not.toBeInTheDocument()
+
+ expect(onDateSelectMock).toHaveBeenLastCalledWith({
+ calendarDateString: null,
+ validation: { valid: true },
+ })
+ })
+
+ it('should validate when Clearing manually (i.e. deleting text not using clear button)', async () => {
+ const onDateSelectMock = jest.fn()
+ const { getByTestId } = render(
+
+ )
+
+ const dateInputString = '2023-10-12'
+ const dateInput = within(
+ getByTestId('dhis2-uicore-input')
+ ).getByRole('textbox')
+
+ userEvent.clear(dateInput)
+ userEvent.type(dateInput, dateInputString)
+ userEvent.tab()
+
+ expect(
+ getByTestId('dhis2-uiwidgets-calendar-inputfield-validation')
+ ).toBeInTheDocument()
+
+ userEvent.clear(dateInput)
+ userEvent.tab()
+
+ expect(onDateSelectMock).toHaveBeenCalledWith({
+ calendarDateString: null,
+ validation: { valid: true },
+ })
+ })
+ })
})
+
+const CalendarWithValidation = (propsFromParent) => {
+ const [date, setDate] = useState()
+
+ const [validation, setValidation] = useState({})
+
+ const errored = () => {
+ if (validation?.error) {
+ return { calendar: validation.validationText }
+ }
+ }
+
+ return (
+
+ )
+ }}
+
+ )
+}
diff --git a/components/calendar/src/calendar-input/calendar-input.js b/components/calendar/src/calendar-input/calendar-input.js
index 4aebe9865e..c8f17c3cb2 100644
--- a/components/calendar/src/calendar-input/calendar-input.js
+++ b/components/calendar/src/calendar-input/calendar-input.js
@@ -57,27 +57,47 @@ export const CalendarInput = ({
[calendar, locale, numberingSystem, weekDayFormat]
)
- const pickerResults = useDatePicker({
- onDateSelect: (result) => {
- const validation = validateDateString(result.calendarDateString, {
- calendar,
- format,
- minDateString: minDate,
- maxDateString: maxDate,
- strictValidation,
+ const onChooseDate = (date, validationOptions) => {
+ // Handling clearing (with clicking the Clear button, or deleting input)
+ if (clearable && (date === null || date === '')) {
+ parentOnDateSelect?.({
+ calendarDateString: null,
+ validation: { valid: true },
})
+ return
+ }
+ // ToDo: This is now a workaround for handling choosing from the date picker
+ // where the blur event gets triggered causing a call with undefined first
+ if (date === undefined) {
+ return
+ }
+
+ const validation = validateDateString(date, validationOptions)
+ parentOnDateSelect?.({
+ calendarDateString: date,
+ validation,
+ })
+ }
+
+ const validationOptions = useMemo(
+ () => ({
+ calendar,
+ format,
+ minDateString: minDate,
+ maxDateString: maxDate,
+ strictValidation,
+ }),
+ [calendar, format, maxDate, minDate, strictValidation]
+ )
+
+ const pickerResults = useDatePicker({
+ onDateSelect: (result) => {
+ onChooseDate(result.calendarDateString, validationOptions)
setOpen(false)
- parentOnDateSelect?.({
- calendarDateString: result.calendarDateString,
- validation,
- })
},
date,
- minDate: minDate,
- maxDate: maxDate,
- strictValidation: strictValidation,
- format: format,
+ ...validationOptions,
options: useDatePickerOptions,
})
@@ -87,15 +107,7 @@ export const CalendarInput = ({
}
const handleBlur = (_, e) => {
- const validation = validateDateString(partialDate, {
- calendar,
- format,
- minDateString: minDate,
- maxDateString: maxDate,
- strictValidation,
- })
- parentOnDateSelect?.({ calendarDateString: partialDate, validation })
-
+ onChooseDate(partialDate, validationOptions)
if (
excludeRef.current &&
!excludeRef.current.contains(e.relatedTarget)
@@ -158,7 +170,7 @@ export const CalendarInput = ({
secondary
small
onClick={() => {
- parentOnDateSelect?.(null)
+ onChooseDate(null)
}}
type="button"
>