From e8257d514f12fee119cf1c549307623801c53ece Mon Sep 17 00:00:00 2001 From: Joakim Bjerknes Date: Thu, 16 Jan 2025 18:51:53 +0100 Subject: [PATCH 01/24] add min max date validation hook --- .../date-picker/DatePickerContext.ts | 1 + .../date-picker/DatePickerInput.tsx | 3 ++ .../date-picker/DatePickerProvider.tsx | 7 ++++ .../hooks/useMinMaxDateValidation.ts | 39 +++++++++++++++++++ 4 files changed, 50 insertions(+) create mode 100644 packages/dnb-eufemia/src/components/date-picker/hooks/useMinMaxDateValidation.ts diff --git a/packages/dnb-eufemia/src/components/date-picker/DatePickerContext.ts b/packages/dnb-eufemia/src/components/date-picker/DatePickerContext.ts index 3c0511fb275..e2194a842f1 100644 --- a/packages/dnb-eufemia/src/components/date-picker/DatePickerContext.ts +++ b/packages/dnb-eufemia/src/components/date-picker/DatePickerContext.ts @@ -23,6 +23,7 @@ export type DatePickerContextValues = ContextProps & translation: ContextProps['translation'] views: Array hasHadValidDate: boolean + minMaxDateValidationMessage: string | undefined previousDateProps: DatePickerDateProps updateDates: ( dates: DatePickerDates, diff --git a/packages/dnb-eufemia/src/components/date-picker/DatePickerInput.tsx b/packages/dnb-eufemia/src/components/date-picker/DatePickerInput.tsx index 85af3f7f050..4c97f33f3cf 100644 --- a/packages/dnb-eufemia/src/components/date-picker/DatePickerInput.tsx +++ b/packages/dnb-eufemia/src/components/date-picker/DatePickerInput.tsx @@ -150,6 +150,7 @@ function DatePickerInput(externalProps: DatePickerInputProps) { updateDates, callOnChangeHandler, hasHadValidDate, + minMaxDateValidationMessage, getReturnObject, __startDay, __startMonth, @@ -960,6 +961,8 @@ function DatePickerInput(externalProps: DatePickerInputProps) { submitAttributes.ref = null } + console.log('minMaxDateValidationMessage', minMaxDateValidationMessage) + return (
{label && {label}} diff --git a/packages/dnb-eufemia/src/components/date-picker/DatePickerProvider.tsx b/packages/dnb-eufemia/src/components/date-picker/DatePickerProvider.tsx index 7732952a302..0de9540efad 100644 --- a/packages/dnb-eufemia/src/components/date-picker/DatePickerProvider.tsx +++ b/packages/dnb-eufemia/src/components/date-picker/DatePickerProvider.tsx @@ -24,6 +24,7 @@ import useDates, { DatePickerDates } from './hooks/useDates' import useLastEventCallCache, { LastEventCallCache, } from './hooks/useLastEventCallCache' +import useMinMaxDateValidation from './hooks/useMinMaxDateValidation' type DatePickerProviderProps = DatePickerAllProps & { setReturnObject: ( @@ -109,6 +110,11 @@ function DatePickerProvider(externalProps: DatePickerProviderProps) { } ) + const minMaxDateValidationMessage = useMinMaxDateValidation({ + ...dates, + isRange: range, + }) + const { views, setViews, forceViewMonthChange } = useViews({ startMonth: dates.startMonth, endMonth: dates.endMonth, @@ -228,6 +234,7 @@ function DatePickerProvider(externalProps: DatePickerProviderProps) { ...dates, previousDateProps, hasHadValidDate, + minMaxDateValidationMessage, views, setViews, forceViewMonthChange, diff --git a/packages/dnb-eufemia/src/components/date-picker/hooks/useMinMaxDateValidation.ts b/packages/dnb-eufemia/src/components/date-picker/hooks/useMinMaxDateValidation.ts new file mode 100644 index 00000000000..88e4046a67f --- /dev/null +++ b/packages/dnb-eufemia/src/components/date-picker/hooks/useMinMaxDateValidation.ts @@ -0,0 +1,39 @@ +import { useMemo } from 'react' +import { useTranslation } from '../../../shared' +import { DatePickerDates } from './useDates' +import { isAfter, isBefore } from 'date-fns' + +export default function useMinMaxDateValidation({ + minDate, + maxDate, + endDate, + startDate, + isRange, +}: Pick< + DatePickerDates, + 'startDate' | 'endDate' | 'minDate' | 'maxDate' +> & { + isRange: boolean +}) { + const translation = useTranslation().DatePicker + + const validationMessage = useMemo(() => { + if (isBefore(startDate, minDate)) { + return translation.errorDateIsBeforeMinDate.replace( + '%s', + minDate.toLocaleDateString() + ) + } + + if (isAfter(startDate, maxDate)) { + return translation.errorDateIsAfterMaxDate.replace( + '%s', + maxDate.toLocaleDateString() + ) + } + + return undefined + }, [startDate, minDate, maxDate, translation]) + + return validationMessage +} From 98ba80b6994161cd257deba26c0895f605b988b3 Mon Sep 17 00:00:00 2001 From: Joakim Bjerknes Date: Thu, 16 Jan 2025 18:52:33 +0100 Subject: [PATCH 02/24] add min max validation messages --- packages/dnb-eufemia/src/shared/locales/en-GB.ts | 4 ++++ packages/dnb-eufemia/src/shared/locales/nb-NO.ts | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/packages/dnb-eufemia/src/shared/locales/en-GB.ts b/packages/dnb-eufemia/src/shared/locales/en-GB.ts index bee90d107ec..f207e107196 100644 --- a/packages/dnb-eufemia/src/shared/locales/en-GB.ts +++ b/packages/dnb-eufemia/src/shared/locales/en-GB.ts @@ -38,6 +38,10 @@ export default { submitButtonText: 'OK', cancelButtonText: 'Cancel', resetButtonText: 'Reset', + errorDateIsBeforeMinDate: 'The date cannot be before %s', + errorDateIsAfterMaxDate: 'The date cannot be after %s', + errorStartDateIsBeforeMinDate: 'Start date cannot be before %s', + errorEndDateIsAfterMaxDate: 'End date cannot be after %s', placeholderCharacters: { day: 'd', month: 'm', diff --git a/packages/dnb-eufemia/src/shared/locales/nb-NO.ts b/packages/dnb-eufemia/src/shared/locales/nb-NO.ts index 1309eb93d70..a9e140c7c67 100644 --- a/packages/dnb-eufemia/src/shared/locales/nb-NO.ts +++ b/packages/dnb-eufemia/src/shared/locales/nb-NO.ts @@ -37,6 +37,10 @@ export default { submitButtonText: 'Ok', cancelButtonText: 'Avbryt', resetButtonText: 'Tilbakestill', + errorDateIsBeforeMinDate: 'Datoen kan ikke være før %s', + errorDateIsAfterMaxDate: 'Datoen kan ikke være etter %s', + errorStartDateIsBeforeMinDate: 'Startdato kan ikke være før %s', + errorEndDateIsAfterMaxDate: 'Sluttdato kan ikke være etter %s', placeholderCharacters: { day: 'd', month: 'm', From 38741f1070be4b82f3680684220efa4e46519d5c Mon Sep 17 00:00:00 2001 From: Joakim Bjerknes Date: Thu, 16 Jan 2025 22:47:09 +0100 Subject: [PATCH 03/24] rename validation hook --- .../src/components/date-picker/DatePickerContext.ts | 2 +- .../src/components/date-picker/DatePickerInput.tsx | 4 ++-- .../src/components/date-picker/DatePickerProvider.tsx | 6 +++--- ...useMinMaxDateValidation.ts => useDateLimitValidation.ts} | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) rename packages/dnb-eufemia/src/components/date-picker/hooks/{useMinMaxDateValidation.ts => useDateLimitValidation.ts} (94%) diff --git a/packages/dnb-eufemia/src/components/date-picker/DatePickerContext.ts b/packages/dnb-eufemia/src/components/date-picker/DatePickerContext.ts index e2194a842f1..74caaae8f0c 100644 --- a/packages/dnb-eufemia/src/components/date-picker/DatePickerContext.ts +++ b/packages/dnb-eufemia/src/components/date-picker/DatePickerContext.ts @@ -23,7 +23,7 @@ export type DatePickerContextValues = ContextProps & translation: ContextProps['translation'] views: Array hasHadValidDate: boolean - minMaxDateValidationMessage: string | undefined + dateLimitValidationMessage: string | undefined previousDateProps: DatePickerDateProps updateDates: ( dates: DatePickerDates, diff --git a/packages/dnb-eufemia/src/components/date-picker/DatePickerInput.tsx b/packages/dnb-eufemia/src/components/date-picker/DatePickerInput.tsx index 4c97f33f3cf..955afb62464 100644 --- a/packages/dnb-eufemia/src/components/date-picker/DatePickerInput.tsx +++ b/packages/dnb-eufemia/src/components/date-picker/DatePickerInput.tsx @@ -150,7 +150,7 @@ function DatePickerInput(externalProps: DatePickerInputProps) { updateDates, callOnChangeHandler, hasHadValidDate, - minMaxDateValidationMessage, + dateLimitValidationMessage, getReturnObject, __startDay, __startMonth, @@ -961,7 +961,7 @@ function DatePickerInput(externalProps: DatePickerInputProps) { submitAttributes.ref = null } - console.log('minMaxDateValidationMessage', minMaxDateValidationMessage) + console.log('dateLimitValidationMessage', dateLimitValidationMessage) return (
diff --git a/packages/dnb-eufemia/src/components/date-picker/DatePickerProvider.tsx b/packages/dnb-eufemia/src/components/date-picker/DatePickerProvider.tsx index 0de9540efad..be3347e3748 100644 --- a/packages/dnb-eufemia/src/components/date-picker/DatePickerProvider.tsx +++ b/packages/dnb-eufemia/src/components/date-picker/DatePickerProvider.tsx @@ -24,7 +24,7 @@ import useDates, { DatePickerDates } from './hooks/useDates' import useLastEventCallCache, { LastEventCallCache, } from './hooks/useLastEventCallCache' -import useMinMaxDateValidation from './hooks/useMinMaxDateValidation' +import useDateLimitValidation from './hooks/useDateLimitValidation' type DatePickerProviderProps = DatePickerAllProps & { setReturnObject: ( @@ -110,7 +110,7 @@ function DatePickerProvider(externalProps: DatePickerProviderProps) { } ) - const minMaxDateValidationMessage = useMinMaxDateValidation({ + const dateLimitValidationMessage = useDateLimitValidation({ ...dates, isRange: range, }) @@ -234,7 +234,7 @@ function DatePickerProvider(externalProps: DatePickerProviderProps) { ...dates, previousDateProps, hasHadValidDate, - minMaxDateValidationMessage, + dateLimitValidationMessage, views, setViews, forceViewMonthChange, diff --git a/packages/dnb-eufemia/src/components/date-picker/hooks/useMinMaxDateValidation.ts b/packages/dnb-eufemia/src/components/date-picker/hooks/useDateLimitValidation.ts similarity index 94% rename from packages/dnb-eufemia/src/components/date-picker/hooks/useMinMaxDateValidation.ts rename to packages/dnb-eufemia/src/components/date-picker/hooks/useDateLimitValidation.ts index 88e4046a67f..bd70fff9eb6 100644 --- a/packages/dnb-eufemia/src/components/date-picker/hooks/useMinMaxDateValidation.ts +++ b/packages/dnb-eufemia/src/components/date-picker/hooks/useDateLimitValidation.ts @@ -3,7 +3,7 @@ import { useTranslation } from '../../../shared' import { DatePickerDates } from './useDates' import { isAfter, isBefore } from 'date-fns' -export default function useMinMaxDateValidation({ +export default function useDateLimitValidation({ minDate, maxDate, endDate, From 781161e83252190aae9e1b6579e916b3aeae4991 Mon Sep 17 00:00:00 2001 From: Joakim Bjerknes Date: Fri, 17 Jan 2025 09:39:50 +0100 Subject: [PATCH 04/24] rename error messages --- .../date-picker/hooks/useDateLimitValidation.ts | 4 ++-- packages/dnb-eufemia/src/shared/locales/en-GB.ts | 8 ++++---- packages/dnb-eufemia/src/shared/locales/nb-NO.ts | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/dnb-eufemia/src/components/date-picker/hooks/useDateLimitValidation.ts b/packages/dnb-eufemia/src/components/date-picker/hooks/useDateLimitValidation.ts index bd70fff9eb6..28f084f010a 100644 --- a/packages/dnb-eufemia/src/components/date-picker/hooks/useDateLimitValidation.ts +++ b/packages/dnb-eufemia/src/components/date-picker/hooks/useDateLimitValidation.ts @@ -19,14 +19,14 @@ export default function useDateLimitValidation({ const validationMessage = useMemo(() => { if (isBefore(startDate, minDate)) { - return translation.errorDateIsBeforeMinDate.replace( + return translation.errorMinDate.replace( '%s', minDate.toLocaleDateString() ) } if (isAfter(startDate, maxDate)) { - return translation.errorDateIsAfterMaxDate.replace( + return translation.errorMaxDate.replace( '%s', maxDate.toLocaleDateString() ) diff --git a/packages/dnb-eufemia/src/shared/locales/en-GB.ts b/packages/dnb-eufemia/src/shared/locales/en-GB.ts index f207e107196..93447d2c666 100644 --- a/packages/dnb-eufemia/src/shared/locales/en-GB.ts +++ b/packages/dnb-eufemia/src/shared/locales/en-GB.ts @@ -38,10 +38,10 @@ export default { submitButtonText: 'OK', cancelButtonText: 'Cancel', resetButtonText: 'Reset', - errorDateIsBeforeMinDate: 'The date cannot be before %s', - errorDateIsAfterMaxDate: 'The date cannot be after %s', - errorStartDateIsBeforeMinDate: 'Start date cannot be before %s', - errorEndDateIsAfterMaxDate: 'End date cannot be after %s', + errorMinDate: 'The date cannot be before %s', + errorMaxDate: 'The date cannot be after %s', + errorRangeMinDate: 'Start date cannot be before %s', + errorRangeMaxDate: 'End date cannot be after %s', placeholderCharacters: { day: 'd', month: 'm', diff --git a/packages/dnb-eufemia/src/shared/locales/nb-NO.ts b/packages/dnb-eufemia/src/shared/locales/nb-NO.ts index a9e140c7c67..c0e3a7e9ed3 100644 --- a/packages/dnb-eufemia/src/shared/locales/nb-NO.ts +++ b/packages/dnb-eufemia/src/shared/locales/nb-NO.ts @@ -37,10 +37,10 @@ export default { submitButtonText: 'Ok', cancelButtonText: 'Avbryt', resetButtonText: 'Tilbakestill', - errorDateIsBeforeMinDate: 'Datoen kan ikke være før %s', - errorDateIsAfterMaxDate: 'Datoen kan ikke være etter %s', - errorStartDateIsBeforeMinDate: 'Startdato kan ikke være før %s', - errorEndDateIsAfterMaxDate: 'Sluttdato kan ikke være etter %s', + errorMinDate: 'Datoen kan ikke være før %s', + errorMaxDate: 'Datoen kan ikke være etter %s', + errorRangeMinDate: 'Startdato kan ikke være før %s', + errorRangeMaxDate: 'Sluttdato kan ikke være etter %s', placeholderCharacters: { day: 'd', month: 'm', From af3f97fa9f0dc2bb5e04d88236b8984ad2cd0427 Mon Sep 17 00:00:00 2001 From: Joakim Bjerknes Date: Fri, 17 Jan 2025 09:50:52 +0100 Subject: [PATCH 05/24] add error message for range end and start dates --- packages/dnb-eufemia/src/shared/locales/en-GB.ts | 6 ++++-- packages/dnb-eufemia/src/shared/locales/nb-NO.ts | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/dnb-eufemia/src/shared/locales/en-GB.ts b/packages/dnb-eufemia/src/shared/locales/en-GB.ts index 93447d2c666..fce0a2ffee4 100644 --- a/packages/dnb-eufemia/src/shared/locales/en-GB.ts +++ b/packages/dnb-eufemia/src/shared/locales/en-GB.ts @@ -40,8 +40,10 @@ export default { resetButtonText: 'Reset', errorMinDate: 'The date cannot be before %s', errorMaxDate: 'The date cannot be after %s', - errorRangeMinDate: 'Start date cannot be before %s', - errorRangeMaxDate: 'End date cannot be after %s', + errorRangeStartDateMinDate: 'Start date cannot be before %s', + errorRangeStartDateMaxDate: 'Start date cannot be after %s', + errorRangeEndDateMinDate: 'End date cannot be before %s', + errorRangeEndDateMaxDate: 'End date cannot be after %s', placeholderCharacters: { day: 'd', month: 'm', diff --git a/packages/dnb-eufemia/src/shared/locales/nb-NO.ts b/packages/dnb-eufemia/src/shared/locales/nb-NO.ts index c0e3a7e9ed3..85ccd5e60cc 100644 --- a/packages/dnb-eufemia/src/shared/locales/nb-NO.ts +++ b/packages/dnb-eufemia/src/shared/locales/nb-NO.ts @@ -39,8 +39,10 @@ export default { resetButtonText: 'Tilbakestill', errorMinDate: 'Datoen kan ikke være før %s', errorMaxDate: 'Datoen kan ikke være etter %s', - errorRangeMinDate: 'Startdato kan ikke være før %s', - errorRangeMaxDate: 'Sluttdato kan ikke være etter %s', + errorRangeStartDateMinDate: 'Startdato kan ikke være før %s', + errorRangeStartDateMaxDate: 'Startdato kan ikke være etter %s', + errorRangeEndDateMinDate: 'Sluttdato kan ikke være etter %s', + errorRangeEndDateMaxDate: 'Sluttdato kan ikke være etter %s', placeholderCharacters: { day: 'd', month: 'm', From 9ea35b775f87c2c4a6b7a4d0aa2302044e30e95d Mon Sep 17 00:00:00 2001 From: Joakim Bjerknes Date: Mon, 20 Jan 2025 15:30:02 +0100 Subject: [PATCH 06/24] integrate date limit validation message with status props --- .../date-picker/DatePickerContext.ts | 1 - .../date-picker/DatePickerProvider.tsx | 48 +++++-------------- 2 files changed, 11 insertions(+), 38 deletions(-) diff --git a/packages/dnb-eufemia/src/components/date-picker/DatePickerContext.ts b/packages/dnb-eufemia/src/components/date-picker/DatePickerContext.ts index 74caaae8f0c..3c0511fb275 100644 --- a/packages/dnb-eufemia/src/components/date-picker/DatePickerContext.ts +++ b/packages/dnb-eufemia/src/components/date-picker/DatePickerContext.ts @@ -23,7 +23,6 @@ export type DatePickerContextValues = ContextProps & translation: ContextProps['translation'] views: Array hasHadValidDate: boolean - dateLimitValidationMessage: string | undefined previousDateProps: DatePickerDateProps updateDates: ( dates: DatePickerDates, diff --git a/packages/dnb-eufemia/src/components/date-picker/DatePickerProvider.tsx b/packages/dnb-eufemia/src/components/date-picker/DatePickerProvider.tsx index be3347e3748..5ce56046cba 100644 --- a/packages/dnb-eufemia/src/components/date-picker/DatePickerProvider.tsx +++ b/packages/dnb-eufemia/src/components/date-picker/DatePickerProvider.tsx @@ -20,11 +20,10 @@ import DatePickerContext, { DatePickerContextValues, } from './DatePickerContext' import useViews, { CalendarView } from './hooks/useViews' -import useDates, { DatePickerDates } from './hooks/useDates' +import { DatePickerDateProps, DatePickerDates } from './hooks/useDates' import useLastEventCallCache, { LastEventCallCache, } from './hooks/useLastEventCallCache' -import useDateLimitValidation from './hooks/useDateLimitValidation' type DatePickerProviderProps = DatePickerAllProps & { setReturnObject: ( @@ -33,6 +32,10 @@ type DatePickerProviderProps = DatePickerAllProps & { hidePicker?: DatePickerContextValues['hidePicker'] attributes?: DatePickerEventAttributes children: React.ReactNode + dates: DatePickerDates + previousDateProps: DatePickerDateProps + updateDates: (dates: DatePickerDates) => void + hasHadValidDate: boolean } export type DatePickerChangeEvent = DatePickerDates & { @@ -72,16 +75,11 @@ function DatePickerProvider(externalProps: DatePickerProviderProps) { const props = { ...defaultProps, ...externalProps } const { - date, - startDate, - endDate, - startMonth, - endMonth, - minDate, - maxDate, - dateFormat, + dates, + updateDates, + hasHadValidDate, + previousDateProps, range, - correctInvalidDate, attributes, returnFormat: returnFormatProp, children, @@ -92,29 +90,6 @@ function DatePickerProvider(externalProps: DatePickerProviderProps) { const sharedContext = useContext(SharedContext) - const { dates, updateDates, hasHadValidDate, previousDateProps } = - useDates( - { - date, - startDate, - endDate, - startMonth, - endMonth, - minDate, - maxDate, - }, - { - dateFormat: dateFormat, - isRange: range, - shouldCorrectDate: correctInvalidDate, - } - ) - - const dateLimitValidationMessage = useDateLimitValidation({ - ...dates, - isRange: range, - }) - const { views, setViews, forceViewMonthChange } = useViews({ startMonth: dates.startMonth, endMonth: dates.endMonth, @@ -138,7 +113,7 @@ function DatePickerProvider(externalProps: DatePickerProviderProps) { const returnFormat = correctV1Format(returnFormatProp) const startDateIsValid = Boolean(startDate && isValid(startDate)) const endDateIsValid = Boolean(endDate && isValid(endDate)) - const hasMinOrMaxDates = minDate || maxDate + const hasMinOrMaxDates = dates.minDate || dates.maxDate const returnObject: ReturnObject = { event, @@ -185,7 +160,7 @@ function DatePickerProvider(externalProps: DatePickerProviderProps) { : startDateIsValid, } }, - [dates, views, attributes, maxDate, minDate, range, returnFormatProp] + [dates, views, attributes, range, returnFormatProp] ) const callOnChangeHandler = useCallback( @@ -234,7 +209,6 @@ function DatePickerProvider(externalProps: DatePickerProviderProps) { ...dates, previousDateProps, hasHadValidDate, - dateLimitValidationMessage, views, setViews, forceViewMonthChange, From d1ec9e4419fdf87cb7431f16e9b47f9502945caa Mon Sep 17 00:00:00 2001 From: Joakim Bjerknes Date: Tue, 21 Jan 2025 15:55:48 +0100 Subject: [PATCH 07/24] add format to validation --- .../date-picker/DatePickerInput.tsx | 5 +- .../hooks/useDateLimitValidation.ts | 68 ++++++++++++++++--- .../dnb-eufemia/src/shared/locales/en-GB.ts | 12 ++-- .../dnb-eufemia/src/shared/locales/nb-NO.ts | 12 ++-- 4 files changed, 72 insertions(+), 25 deletions(-) diff --git a/packages/dnb-eufemia/src/components/date-picker/DatePickerInput.tsx b/packages/dnb-eufemia/src/components/date-picker/DatePickerInput.tsx index 955afb62464..f7a8e33a456 100644 --- a/packages/dnb-eufemia/src/components/date-picker/DatePickerInput.tsx +++ b/packages/dnb-eufemia/src/components/date-picker/DatePickerInput.tsx @@ -150,7 +150,6 @@ function DatePickerInput(externalProps: DatePickerInputProps) { updateDates, callOnChangeHandler, hasHadValidDate, - dateLimitValidationMessage, getReturnObject, __startDay, __startMonth, @@ -309,7 +308,7 @@ function DatePickerInput(externalProps: DatePickerInputProps) { break } } - + console.log('possibleFormats', possibleFormats) const mode = focusMode.current === 'start' ? 'startDate' : 'endDate' @@ -961,8 +960,6 @@ function DatePickerInput(externalProps: DatePickerInputProps) { submitAttributes.ref = null } - console.log('dateLimitValidationMessage', dateLimitValidationMessage) - return (
{label && {label}} diff --git a/packages/dnb-eufemia/src/components/date-picker/hooks/useDateLimitValidation.ts b/packages/dnb-eufemia/src/components/date-picker/hooks/useDateLimitValidation.ts index 28f084f010a..2610e663f94 100644 --- a/packages/dnb-eufemia/src/components/date-picker/hooks/useDateLimitValidation.ts +++ b/packages/dnb-eufemia/src/components/date-picker/hooks/useDateLimitValidation.ts @@ -1,7 +1,8 @@ import { useMemo } from 'react' import { useTranslation } from '../../../shared' import { DatePickerDates } from './useDates' -import { isAfter, isBefore } from 'date-fns' +import { format, isAfter, isBefore } from 'date-fns' +import { DatePickerProps } from '../DatePicker' export default function useDateLimitValidation({ minDate, @@ -9,31 +10,80 @@ export default function useDateLimitValidation({ endDate, startDate, isRange, + dateFormat, }: Pick< DatePickerDates, 'startDate' | 'endDate' | 'minDate' | 'maxDate' > & { - isRange: boolean + isRange: DatePickerProps['range'] + dateFormat: DatePickerProps['dateFormat'] }) { const translation = useTranslation().DatePicker const validationMessage = useMemo(() => { + if (!minDate && !maxDate) { + return undefined + } + + // Handle non range validation + if (!isRange) { + if (isBefore(startDate, minDate)) { + return translation.errorMinDate.replace( + /%s/, + format(minDate, dateFormat) + ) + } + + if (isAfter(startDate, maxDate)) { + return translation.errorMaxDate.replace( + /%s/, + format(maxDate, dateFormat) + ) + } + } + + let validationMessage = '' + if (isBefore(startDate, minDate)) { - return translation.errorMinDate.replace( - '%s', - minDate.toLocaleDateString() + validationMessage += translation.errorRangeStartDateMinDate.replace( + /%s/, + format(minDate, dateFormat) ) } if (isAfter(startDate, maxDate)) { - return translation.errorMaxDate.replace( - '%s', - maxDate.toLocaleDateString() + validationMessage += translation.errorRangeStartDateMaxDate.replace( + /%s/, + format(maxDate, dateFormat) ) } + if (isBefore(endDate, minDate)) { + validationMessage += translation.errorRangeEndDateMinDate.replace( + /%s/, + format(minDate, dateFormat) + ) + } + + if (isAfter(endDate, maxDate)) { + validationMessage += translation.errorRangeEndDateMaxDate.replace( + /%s/, + format(maxDate, dateFormat) + ) + + return validationMessage || undefined + } + return undefined - }, [startDate, minDate, maxDate, translation]) + }, [ + startDate, + endDate, + minDate, + maxDate, + isRange, + dateFormat, + translation, + ]) return validationMessage } diff --git a/packages/dnb-eufemia/src/shared/locales/en-GB.ts b/packages/dnb-eufemia/src/shared/locales/en-GB.ts index fce0a2ffee4..2a17ef1f1a8 100644 --- a/packages/dnb-eufemia/src/shared/locales/en-GB.ts +++ b/packages/dnb-eufemia/src/shared/locales/en-GB.ts @@ -38,12 +38,12 @@ export default { submitButtonText: 'OK', cancelButtonText: 'Cancel', resetButtonText: 'Reset', - errorMinDate: 'The date cannot be before %s', - errorMaxDate: 'The date cannot be after %s', - errorRangeStartDateMinDate: 'Start date cannot be before %s', - errorRangeStartDateMaxDate: 'Start date cannot be after %s', - errorRangeEndDateMinDate: 'End date cannot be before %s', - errorRangeEndDateMaxDate: 'End date cannot be after %s', + errorMinDate: 'Chosen date must be after %s', + errorMaxDate: 'Chosen date must be before %s', + errorRangeStartDateMinDate: 'Chosen start date must be after %s', + errorRangeStartDateMaxDate: 'Chosen start date must be before %s', + errorRangeEndDateMinDate: 'Chosen end date must be after %s', + errorRangeEndDateMaxDate: 'Chosen end date must be before %s', placeholderCharacters: { day: 'd', month: 'm', diff --git a/packages/dnb-eufemia/src/shared/locales/nb-NO.ts b/packages/dnb-eufemia/src/shared/locales/nb-NO.ts index 85ccd5e60cc..9a5ddc6e0b1 100644 --- a/packages/dnb-eufemia/src/shared/locales/nb-NO.ts +++ b/packages/dnb-eufemia/src/shared/locales/nb-NO.ts @@ -37,12 +37,12 @@ export default { submitButtonText: 'Ok', cancelButtonText: 'Avbryt', resetButtonText: 'Tilbakestill', - errorMinDate: 'Datoen kan ikke være før %s', - errorMaxDate: 'Datoen kan ikke være etter %s', - errorRangeStartDateMinDate: 'Startdato kan ikke være før %s', - errorRangeStartDateMaxDate: 'Startdato kan ikke være etter %s', - errorRangeEndDateMinDate: 'Sluttdato kan ikke være etter %s', - errorRangeEndDateMaxDate: 'Sluttdato kan ikke være etter %s', + errorMinDate: 'Valgt dato må være etter %s', + errorMaxDate: 'Valgt dato må være før %s', + errorRangeStartDateMinDate: 'Valgt startdato må være etter %s', + errorRangeStartDateMaxDate: 'Valgt startdato må være før %s', + errorRangeEndDateMinDate: 'Valgt sluttdato må være etter %s', + errorRangeEndDateMaxDate: 'Valgt sluttdato må være før %s', placeholderCharacters: { day: 'd', month: 'm', From fae8346a531d10e1551e43abd1725892d12883b9 Mon Sep 17 00:00:00 2001 From: Joakim Bjerknes Date: Tue, 21 Jan 2025 17:13:01 +0100 Subject: [PATCH 08/24] add possiblity to merge status and dateLimitValidation message in one error block --- .../dnb-eufemia/src/components/form-status/FormStatus.d.ts | 6 +----- packages/dnb-eufemia/src/shared/locales/en-GB.ts | 1 + packages/dnb-eufemia/src/shared/locales/nb-NO.ts | 1 + 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/dnb-eufemia/src/components/form-status/FormStatus.d.ts b/packages/dnb-eufemia/src/components/form-status/FormStatus.d.ts index 185ee1c898a..201b1cdffdd 100644 --- a/packages/dnb-eufemia/src/components/form-status/FormStatus.d.ts +++ b/packages/dnb-eufemia/src/components/form-status/FormStatus.d.ts @@ -3,11 +3,7 @@ import type { GlobalStatusConfigObject } from '../GlobalStatus'; import type { IconIcon, IconSize } from '../Icon'; import type { SkeletonShow } from '../Skeleton'; import type { SpacingProps, SpaceTypeAll } from '../space/types'; -export type FormStatusText = - | string - | boolean - | ((...args: any[]) => any) - | React.ReactNode; +export type FormStatusText = React.ReactNode; export type FormStatusState = | boolean | string diff --git a/packages/dnb-eufemia/src/shared/locales/en-GB.ts b/packages/dnb-eufemia/src/shared/locales/en-GB.ts index 2a17ef1f1a8..dd325690632 100644 --- a/packages/dnb-eufemia/src/shared/locales/en-GB.ts +++ b/packages/dnb-eufemia/src/shared/locales/en-GB.ts @@ -38,6 +38,7 @@ export default { submitButtonText: 'OK', cancelButtonText: 'Cancel', resetButtonText: 'Reset', + errorSummary: 'Please correct the following errors:', errorMinDate: 'Chosen date must be after %s', errorMaxDate: 'Chosen date must be before %s', errorRangeStartDateMinDate: 'Chosen start date must be after %s', diff --git a/packages/dnb-eufemia/src/shared/locales/nb-NO.ts b/packages/dnb-eufemia/src/shared/locales/nb-NO.ts index 9a5ddc6e0b1..b9b223371ac 100644 --- a/packages/dnb-eufemia/src/shared/locales/nb-NO.ts +++ b/packages/dnb-eufemia/src/shared/locales/nb-NO.ts @@ -37,6 +37,7 @@ export default { submitButtonText: 'Ok', cancelButtonText: 'Avbryt', resetButtonText: 'Tilbakestill', + errorSummary: 'Feil som må rettes:', errorMinDate: 'Valgt dato må være etter %s', errorMaxDate: 'Valgt dato må være før %s', errorRangeStartDateMinDate: 'Valgt startdato må være etter %s', From 39e37f636eaf6bef35cc5e216799863031d14d0c Mon Sep 17 00:00:00 2001 From: Joakim Bjerknes Date: Tue, 21 Jan 2025 17:41:43 +0100 Subject: [PATCH 09/24] make date limit validation range compliant --- ...lidation.ts => useDateLimitValidation.tsx} | 50 +++++++++++++------ 1 file changed, 34 insertions(+), 16 deletions(-) rename packages/dnb-eufemia/src/components/date-picker/hooks/{useDateLimitValidation.ts => useDateLimitValidation.tsx} (61%) diff --git a/packages/dnb-eufemia/src/components/date-picker/hooks/useDateLimitValidation.ts b/packages/dnb-eufemia/src/components/date-picker/hooks/useDateLimitValidation.tsx similarity index 61% rename from packages/dnb-eufemia/src/components/date-picker/hooks/useDateLimitValidation.ts rename to packages/dnb-eufemia/src/components/date-picker/hooks/useDateLimitValidation.tsx index 2610e663f94..3e9cd565af5 100644 --- a/packages/dnb-eufemia/src/components/date-picker/hooks/useDateLimitValidation.ts +++ b/packages/dnb-eufemia/src/components/date-picker/hooks/useDateLimitValidation.tsx @@ -3,6 +3,7 @@ import { useTranslation } from '../../../shared' import { DatePickerDates } from './useDates' import { format, isAfter, isBefore } from 'date-fns' import { DatePickerProps } from '../DatePicker' +import { Li, Ul } from '../../../elements' export default function useDateLimitValidation({ minDate, @@ -42,39 +43,56 @@ export default function useDateLimitValidation({ } } - let validationMessage = '' + const messages = [] if (isBefore(startDate, minDate)) { - validationMessage += translation.errorRangeStartDateMinDate.replace( - /%s/, - format(minDate, dateFormat) + messages.push( + translation.errorRangeStartDateMinDate.replace( + /%s/, + format(minDate, dateFormat) + ) ) } if (isAfter(startDate, maxDate)) { - validationMessage += translation.errorRangeStartDateMaxDate.replace( - /%s/, - format(maxDate, dateFormat) + messages.push( + translation.errorRangeStartDateMaxDate.replace( + /%s/, + format(maxDate, dateFormat) + ) ) } if (isBefore(endDate, minDate)) { - validationMessage += translation.errorRangeEndDateMinDate.replace( - /%s/, - format(minDate, dateFormat) + messages.push( + translation.errorRangeEndDateMinDate.replace( + /%s/, + format(minDate, dateFormat) + ) ) } if (isAfter(endDate, maxDate)) { - validationMessage += translation.errorRangeEndDateMaxDate.replace( - /%s/, - format(maxDate, dateFormat) + messages.push( + translation.errorRangeEndDateMaxDate.replace( + /%s/, + format(maxDate, dateFormat) + ) ) - - return validationMessage || undefined } - return undefined + return messages.length > 1 ? ( + <> + {translation.errorSummary} +
    + {messages.map((status, i) => { + return
  • {status}
  • + })} +
+ + ) : ( + messages[0] || undefined + ) }, [ startDate, endDate, From 04202120b385336ad607eb2f2deaf7e0de8f932f Mon Sep 17 00:00:00 2001 From: Joakim Bjerknes Date: Wed, 22 Jan 2025 08:49:02 +0100 Subject: [PATCH 10/24] make useDateLimitValidation return an object --- .../hooks/useDateLimitValidation.tsx | 67 +++++++++++++------ 1 file changed, 46 insertions(+), 21 deletions(-) diff --git a/packages/dnb-eufemia/src/components/date-picker/hooks/useDateLimitValidation.tsx b/packages/dnb-eufemia/src/components/date-picker/hooks/useDateLimitValidation.tsx index 3e9cd565af5..94d1b14ffaf 100644 --- a/packages/dnb-eufemia/src/components/date-picker/hooks/useDateLimitValidation.tsx +++ b/packages/dnb-eufemia/src/components/date-picker/hooks/useDateLimitValidation.tsx @@ -5,6 +5,11 @@ import { format, isAfter, isBefore } from 'date-fns' import { DatePickerProps } from '../DatePicker' import { Li, Ul } from '../../../elements' +type DateLimitValidation = { + status: DatePickerProps['status'] + statusState: DatePickerProps['statusState'] +} + export default function useDateLimitValidation({ minDate, maxDate, @@ -21,25 +26,35 @@ export default function useDateLimitValidation({ }) { const translation = useTranslation().DatePicker - const validationMessage = useMemo(() => { + const validationMessage = useMemo< + DateLimitValidation | undefined + >(() => { if (!minDate && !maxDate) { return undefined } + const statusState = 'error' + // Handle non range validation if (!isRange) { if (isBefore(startDate, minDate)) { - return translation.errorMinDate.replace( - /%s/, - format(minDate, dateFormat) - ) + return { + status: translation.errorMinDate.replace( + /%s/, + format(minDate, dateFormat) + ), + statusState, + } } if (isAfter(startDate, maxDate)) { - return translation.errorMaxDate.replace( - /%s/, - format(maxDate, dateFormat) - ) + return { + status: translation.errorMaxDate.replace( + /%s/, + format(maxDate, dateFormat) + ), + statusState, + } } } @@ -81,18 +96,12 @@ export default function useDateLimitValidation({ ) } - return messages.length > 1 ? ( - <> - {translation.errorSummary} -
    - {messages.map((status, i) => { - return
  • {status}
  • - })} -
- - ) : ( - messages[0] || undefined - ) + const status = + messages.length > 1 + ? combineErrorMessages(translation.errorRangeTitle, messages) + : messages[0] + + return status ? { status, statusState } : undefined }, [ startDate, endDate, @@ -105,3 +114,19 @@ export default function useDateLimitValidation({ return validationMessage } + +export function combineErrorMessages( + title: string, + messages: React.ReactNode[] +) { + return ( + <> + {title} +
    + {messages.map((status, i) => { + return
  • {status}
  • + })} +
+ + ) +} From f897cb9be93838a2c80c671c145caef7dcb763d7 Mon Sep 17 00:00:00 2001 From: Joakim Bjerknes Date: Wed, 22 Jan 2025 10:44:31 +0100 Subject: [PATCH 11/24] add correct title --- .../components/date-picker/hooks/useDateLimitValidation.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/dnb-eufemia/src/components/date-picker/hooks/useDateLimitValidation.tsx b/packages/dnb-eufemia/src/components/date-picker/hooks/useDateLimitValidation.tsx index 94d1b14ffaf..b276a7a28cb 100644 --- a/packages/dnb-eufemia/src/components/date-picker/hooks/useDateLimitValidation.tsx +++ b/packages/dnb-eufemia/src/components/date-picker/hooks/useDateLimitValidation.tsx @@ -1,4 +1,4 @@ -import { useMemo } from 'react' +import React, { useMemo } from 'react' import { useTranslation } from '../../../shared' import { DatePickerDates } from './useDates' import { format, isAfter, isBefore } from 'date-fns' @@ -98,7 +98,7 @@ export default function useDateLimitValidation({ const status = messages.length > 1 - ? combineErrorMessages(translation.errorRangeTitle, messages) + ? combineErrorMessages(translation.errorSummary, messages) : messages[0] return status ? { status, statusState } : undefined From 0361466211410245ff79560fb0dfd9b4a7ae65f0 Mon Sep 17 00:00:00 2001 From: Joakim Bjerknes Date: Wed, 22 Jan 2025 12:47:31 +0100 Subject: [PATCH 12/24] add tests --- .../date-picker/__tests__/DatePicker.test.tsx | 448 ++++++++++++++++++ 1 file changed, 448 insertions(+) diff --git a/packages/dnb-eufemia/src/components/date-picker/__tests__/DatePicker.test.tsx b/packages/dnb-eufemia/src/components/date-picker/__tests__/DatePicker.test.tsx index 5447e313e33..093e61a77bc 100644 --- a/packages/dnb-eufemia/src/components/date-picker/__tests__/DatePicker.test.tsx +++ b/packages/dnb-eufemia/src/components/date-picker/__tests__/DatePicker.test.tsx @@ -25,6 +25,11 @@ import { } from '../DatePickerCalc' import { fireEvent, render, waitFor, screen } from '@testing-library/react' import { Provider } from '../../../shared' +import nbNO from '../../../shared/locales/nb-NO' +import enGB from '../../../shared/locales/en-GB' + +const nb = nbNO['nb-NO'].DatePicker +const en = enGB['en-GB'].DatePicker describe('DatePicker component', () => { it('renders with props as an object', () => { @@ -1408,6 +1413,342 @@ describe('DatePicker component', () => { expect(onChange.mock.calls[4][0].is_valid).toBe(true) }) + it('should display error message if `date` is before `minDate`', async () => { + const minDate = '2025-01-01' + + render() + + const day = document.querySelector( + '.dnb-date-picker__input--day' + ) as HTMLInputElement + + expect( + document.querySelector('.dnb-form-status--error') + ).not.toBeInTheDocument() + + await userEvent.click(day) + await userEvent.keyboard('{ArrowDown}') + + expect( + document.querySelector('.dnb-form-status--error') + ).toBeInTheDocument() + + expect(screen.getByRole('alert')).toHaveTextContent( + nb.errorMinDate.replace(/%s/, minDate) + ) + }) + + it('should display error message if `date` is after `maxDate`', async () => { + const maxDate = '2025-01-31' + + render() + + const day = document.querySelector( + '.dnb-date-picker__input--day' + ) as HTMLInputElement + + expect( + document.querySelector('.dnb-form-status--error') + ).not.toBeInTheDocument() + + await userEvent.click(day) + await userEvent.keyboard('{ArrowUp}') + + expect( + document.querySelector('.dnb-form-status--error') + ).toBeInTheDocument() + + expect(screen.getByRole('alert')).toHaveTextContent( + nb.errorMaxDate.replace(/%s/, maxDate) + ) + }) + + it('should display error message if `startDate` is before `minDate`', async () => { + const minDate = '2025-01-01' + + render( + + ) + + const [startDay] = Array.from( + document.querySelectorAll('.dnb-date-picker__input--day') + ) as Array + + expect( + document.querySelector('.dnb-form-status--error') + ).not.toBeInTheDocument() + + await userEvent.click(startDay) + await userEvent.keyboard('{ArrowDown}') + + expect( + document.querySelector('.dnb-form-status--error') + ).toBeInTheDocument() + + expect(screen.getByRole('alert')).toHaveTextContent( + nb.errorRangeStartDateMinDate.replace(/%s/, minDate) + ) + }) + + it('should display error message if `startDate` is after `maxDate`', async () => { + const maxDate = '2025-01-31' + + render( + + ) + + const [startDay] = Array.from( + document.querySelectorAll('.dnb-date-picker__input--day') + ) as Array + + expect( + document.querySelector('.dnb-form-status--error') + ).not.toBeInTheDocument() + + await userEvent.click(startDay) + await userEvent.keyboard('{ArrowUp}') + + expect( + document.querySelector('.dnb-form-status--error') + ).toBeInTheDocument() + + expect(screen.getByRole('alert')).toHaveTextContent( + nb.errorRangeStartDateMaxDate.replace(/%s/, maxDate) + ) + }) + + it('should display error message if `endDate` is before `minDate`', async () => { + const minDate = '2025-01-01' + + render( + + ) + + const [, endDay] = Array.from( + document.querySelectorAll('.dnb-date-picker__input--day') + ) as Array + + expect( + document.querySelector('.dnb-form-status--error') + ).not.toBeInTheDocument() + + await userEvent.click(endDay) + await userEvent.keyboard('{ArrowDown}') + + expect( + document.querySelector('.dnb-form-status--error') + ).toBeInTheDocument() + + expect(screen.getByRole('alert')).toHaveTextContent( + nb.errorRangeEndDateMinDate.replace(/%s/, minDate) + ) + }) + + it('should display error message if `endDate` is after `maxDate`', async () => { + const maxDate = '2025-01-31' + + render( + + ) + + const [, endDay] = Array.from( + document.querySelectorAll('.dnb-date-picker__input--day') + ) as Array + + expect( + document.querySelector('.dnb-form-status--error') + ).not.toBeInTheDocument() + + await userEvent.click(endDay) + await userEvent.keyboard('{ArrowUp}') + + expect( + document.querySelector('.dnb-form-status--error') + ).toBeInTheDocument() + + expect(screen.getByRole('alert')).toHaveTextContent( + nb.errorRangeEndDateMaxDate.replace(/%s/, maxDate) + ) + }) + + it('should display error messages if `startDate` and `endDate` date is outside of limits in `range` mode', async () => { + const minDate = '2025-01-01' + const maxDate = '2025-01-31' + + render( + + ) + + const [startDay, startMonth, , endDay, endMonth] = Array.from( + document.querySelectorAll('.dnb-date-picker__input') + ) as Array + + expect( + document.querySelector('.dnb-form-status--error') + ).not.toBeInTheDocument() + + // Focus on day inputs and select out of limit dates + await userEvent.click(startDay) + await userEvent.keyboard('{ArrowDown}') + await userEvent.click(endDay) + await userEvent.keyboard('{ArrowUp}') + + expect( + document.querySelector('.dnb-form-status--error') + ).toBeInTheDocument() + + const statusText = document.querySelector('.dnb-form-status__text') + const messages = () => + Array.from( + statusText.querySelectorAll('.dnb-li') + ) as Array + + expect(statusText).toHaveTextContent(nb.errorSummary) + + expect(messages()[0]).toHaveTextContent( + nb.errorRangeStartDateMinDate.replace(/%s/, minDate) + ) + expect(messages()[1]).toHaveTextContent( + nb.errorRangeEndDateMaxDate.replace(/%s/, maxDate) + ) + + await userEvent.click(startMonth) + await userEvent.keyboard('{ArrowUp>2}') + + expect(messages()[0]).toHaveTextContent( + nb.errorRangeStartDateMaxDate.replace(/%s/, maxDate) + ) + expect(messages()[1]).toHaveTextContent( + nb.errorRangeEndDateMaxDate.replace(/%s/, maxDate) + ) + + await userEvent.click(endMonth) + await userEvent.keyboard('{ArrowDown>2}') + + expect(messages()[0]).toHaveTextContent( + nb.errorRangeStartDateMaxDate.replace(/%s/, maxDate) + ) + expect(messages()[1]).toHaveTextContent( + nb.errorRangeEndDateMinDate.replace(/%s/, minDate) + ) + + await userEvent.click(startMonth) + await userEvent.keyboard('{ArrowDown>2}') + + expect(messages()[0]).toHaveTextContent( + nb.errorRangeStartDateMinDate.replace(/%s/, minDate) + ) + expect(messages()[1]).toHaveTextContent( + nb.errorRangeEndDateMinDate.replace(/%s/, minDate) + ) + + await userEvent.click(startMonth) + await userEvent.keyboard('{ArrowUp}') + await userEvent.click(endMonth) + await userEvent.keyboard('{ArrowUp}') + + expect( + document.querySelector('.dnb-form-status--error') + ).not.toBeInTheDocument() + }) + + it('should validate date limits based on `date` prop', async () => { + const minDate = '2025-01-01' + const maxDate = '2025-01-31' + + // Test min date + const { rerender } = render( + + ) + + const day = document.querySelector( + '.dnb-date-picker__input--day' + ) as HTMLInputElement + + expect( + document.querySelector('.dnb-form-status--error') + ).toBeInTheDocument() + + expect(screen.getByRole('alert')).toHaveTextContent( + nb.errorMinDate.replace(/%s/, minDate) + ) + + await userEvent.click(day) + await userEvent.keyboard('{ArrowUp}') + + expect( + document.querySelector('.dnb-form-status--error') + ).not.toBeInTheDocument() + + // Test max date + rerender() + + expect( + document.querySelector('.dnb-form-status--error') + ).toBeInTheDocument() + + expect(screen.getByRole('alert')).toHaveTextContent( + nb.errorMaxDate.replace(/%s/, maxDate) + ) + + await userEvent.click(day) + await userEvent.keyboard('{ArrowDown}') + + expect( + document.querySelector('.dnb-form-status--error') + ).not.toBeInTheDocument() + }) + + it('should validate date limits based on `startDate` and `endDate` props', async () => { + const minDate = '2025-01-01' + const maxDate = '2025-01-31' + + render( + + ) + + const [startDay, endDay] = Array.from( + document.querySelectorAll('.dnb-date-picker__input--day') + ) as Array + + const [startDayError, endDayError] = Array.from( + document.querySelectorAll('.dnb-li') + ) as Array + + expect( + document.querySelector('.dnb-form-status--error') + ).toBeInTheDocument() + + expect(startDayError).toHaveTextContent( + nb.errorRangeStartDateMinDate.replace(/%s/, minDate) + ) + expect(endDayError).toHaveTextContent( + nb.errorRangeEndDateMaxDate.replace(/%s/, maxDate) + ) + + await userEvent.click(startDay) + await userEvent.keyboard('{ArrowUp}') + await userEvent.click(endDay) + await userEvent.keyboard('{ArrowDown}') + + expect( + document.querySelector('.dnb-form-status--error') + ).not.toBeInTheDocument() + }) + it('has to auto-correct invalid min/max dates', async () => { const onChange = jest.fn() @@ -1450,6 +1791,113 @@ describe('DatePicker component', () => { ) }) + it('should display date limit error messages based on locale', async () => { + const minDate = '2025-01-01' + const maxDate = '2025-01-31' + + const { rerender } = render( + + + + ) + + const [day, month] = Array.from( + document.querySelectorAll('.dnb-date-picker__input') + ) + + await userEvent.click(day) + await userEvent.keyboard('{ArrowDown}') + + expect( + document.querySelector('.dnb-form-status--error') + ).toBeInTheDocument() + expect(screen.getByRole('alert')).toHaveTextContent( + en.errorMinDate.replace(/%s/, minDate) + ) + + await userEvent.click(month) + await userEvent.keyboard('{ArrowUp>2}') + + expect(screen.getByRole('alert')).toHaveTextContent( + en.errorMaxDate.replace(/%s/, maxDate) + ) + + rerender( + + + + ) + + const [, startMonth, , , endMonth] = Array.from( + document.querySelectorAll('.dnb-date-picker__input') + ) as Array + + await userEvent.click(startMonth) + await userEvent.keyboard('{ArrowDown}') + await userEvent.click(endMonth) + await userEvent.keyboard('{ArrowUp}') + + expect( + document.querySelector('.dnb-form-status--error') + ).toBeInTheDocument() + + const messages = () => + Array.from( + document.querySelectorAll('.dnb-form-status--error .dnb-li') + ) as Array + + expect(screen.getByRole('alert')).toHaveTextContent(en.errorSummary) + + expect(messages()[0]).toHaveTextContent( + en.errorRangeStartDateMinDate.replace(/%s/, minDate) + ) + expect(messages()[1]).toHaveTextContent( + en.errorRangeEndDateMaxDate.replace(/%s/, maxDate) + ) + + await userEvent.click(startMonth) + await userEvent.keyboard('{ArrowUp>2}') + + expect(messages()[0]).toHaveTextContent( + en.errorRangeStartDateMaxDate.replace(/%s/, maxDate) + ) + expect(messages()[1]).toHaveTextContent( + en.errorRangeEndDateMaxDate.replace(/%s/, maxDate) + ) + + await userEvent.click(endMonth) + await userEvent.keyboard('{ArrowDown>2}') + + expect(messages()[0]).toHaveTextContent( + en.errorRangeStartDateMaxDate.replace(/%s/, maxDate) + ) + expect(messages()[1]).toHaveTextContent( + en.errorRangeEndDateMinDate.replace(/%s/, minDate) + ) + + await userEvent.click(startMonth) + await userEvent.keyboard('{ArrowDown>2}') + + expect(messages()[0]).toHaveTextContent( + en.errorRangeStartDateMinDate.replace(/%s/, minDate) + ) + expect(messages()[1]).toHaveTextContent( + en.errorRangeEndDateMinDate.replace(/%s/, minDate) + ) + }) + it('has to auto-correct invalid date based on min date', async () => { render( Date: Wed, 22 Jan 2025 13:00:41 +0100 Subject: [PATCH 13/24] move dates do own describe to prevent provider to mess up other tests --- .../date-picker/__tests__/DatePicker.test.tsx | 898 +++++++++--------- 1 file changed, 455 insertions(+), 443 deletions(-) diff --git a/packages/dnb-eufemia/src/components/date-picker/__tests__/DatePicker.test.tsx b/packages/dnb-eufemia/src/components/date-picker/__tests__/DatePicker.test.tsx index 093e61a77bc..3f9fdcef8fc 100644 --- a/packages/dnb-eufemia/src/components/date-picker/__tests__/DatePicker.test.tsx +++ b/packages/dnb-eufemia/src/components/date-picker/__tests__/DatePicker.test.tsx @@ -1413,342 +1413,6 @@ describe('DatePicker component', () => { expect(onChange.mock.calls[4][0].is_valid).toBe(true) }) - it('should display error message if `date` is before `minDate`', async () => { - const minDate = '2025-01-01' - - render() - - const day = document.querySelector( - '.dnb-date-picker__input--day' - ) as HTMLInputElement - - expect( - document.querySelector('.dnb-form-status--error') - ).not.toBeInTheDocument() - - await userEvent.click(day) - await userEvent.keyboard('{ArrowDown}') - - expect( - document.querySelector('.dnb-form-status--error') - ).toBeInTheDocument() - - expect(screen.getByRole('alert')).toHaveTextContent( - nb.errorMinDate.replace(/%s/, minDate) - ) - }) - - it('should display error message if `date` is after `maxDate`', async () => { - const maxDate = '2025-01-31' - - render() - - const day = document.querySelector( - '.dnb-date-picker__input--day' - ) as HTMLInputElement - - expect( - document.querySelector('.dnb-form-status--error') - ).not.toBeInTheDocument() - - await userEvent.click(day) - await userEvent.keyboard('{ArrowUp}') - - expect( - document.querySelector('.dnb-form-status--error') - ).toBeInTheDocument() - - expect(screen.getByRole('alert')).toHaveTextContent( - nb.errorMaxDate.replace(/%s/, maxDate) - ) - }) - - it('should display error message if `startDate` is before `minDate`', async () => { - const minDate = '2025-01-01' - - render( - - ) - - const [startDay] = Array.from( - document.querySelectorAll('.dnb-date-picker__input--day') - ) as Array - - expect( - document.querySelector('.dnb-form-status--error') - ).not.toBeInTheDocument() - - await userEvent.click(startDay) - await userEvent.keyboard('{ArrowDown}') - - expect( - document.querySelector('.dnb-form-status--error') - ).toBeInTheDocument() - - expect(screen.getByRole('alert')).toHaveTextContent( - nb.errorRangeStartDateMinDate.replace(/%s/, minDate) - ) - }) - - it('should display error message if `startDate` is after `maxDate`', async () => { - const maxDate = '2025-01-31' - - render( - - ) - - const [startDay] = Array.from( - document.querySelectorAll('.dnb-date-picker__input--day') - ) as Array - - expect( - document.querySelector('.dnb-form-status--error') - ).not.toBeInTheDocument() - - await userEvent.click(startDay) - await userEvent.keyboard('{ArrowUp}') - - expect( - document.querySelector('.dnb-form-status--error') - ).toBeInTheDocument() - - expect(screen.getByRole('alert')).toHaveTextContent( - nb.errorRangeStartDateMaxDate.replace(/%s/, maxDate) - ) - }) - - it('should display error message if `endDate` is before `minDate`', async () => { - const minDate = '2025-01-01' - - render( - - ) - - const [, endDay] = Array.from( - document.querySelectorAll('.dnb-date-picker__input--day') - ) as Array - - expect( - document.querySelector('.dnb-form-status--error') - ).not.toBeInTheDocument() - - await userEvent.click(endDay) - await userEvent.keyboard('{ArrowDown}') - - expect( - document.querySelector('.dnb-form-status--error') - ).toBeInTheDocument() - - expect(screen.getByRole('alert')).toHaveTextContent( - nb.errorRangeEndDateMinDate.replace(/%s/, minDate) - ) - }) - - it('should display error message if `endDate` is after `maxDate`', async () => { - const maxDate = '2025-01-31' - - render( - - ) - - const [, endDay] = Array.from( - document.querySelectorAll('.dnb-date-picker__input--day') - ) as Array - - expect( - document.querySelector('.dnb-form-status--error') - ).not.toBeInTheDocument() - - await userEvent.click(endDay) - await userEvent.keyboard('{ArrowUp}') - - expect( - document.querySelector('.dnb-form-status--error') - ).toBeInTheDocument() - - expect(screen.getByRole('alert')).toHaveTextContent( - nb.errorRangeEndDateMaxDate.replace(/%s/, maxDate) - ) - }) - - it('should display error messages if `startDate` and `endDate` date is outside of limits in `range` mode', async () => { - const minDate = '2025-01-01' - const maxDate = '2025-01-31' - - render( - - ) - - const [startDay, startMonth, , endDay, endMonth] = Array.from( - document.querySelectorAll('.dnb-date-picker__input') - ) as Array - - expect( - document.querySelector('.dnb-form-status--error') - ).not.toBeInTheDocument() - - // Focus on day inputs and select out of limit dates - await userEvent.click(startDay) - await userEvent.keyboard('{ArrowDown}') - await userEvent.click(endDay) - await userEvent.keyboard('{ArrowUp}') - - expect( - document.querySelector('.dnb-form-status--error') - ).toBeInTheDocument() - - const statusText = document.querySelector('.dnb-form-status__text') - const messages = () => - Array.from( - statusText.querySelectorAll('.dnb-li') - ) as Array - - expect(statusText).toHaveTextContent(nb.errorSummary) - - expect(messages()[0]).toHaveTextContent( - nb.errorRangeStartDateMinDate.replace(/%s/, minDate) - ) - expect(messages()[1]).toHaveTextContent( - nb.errorRangeEndDateMaxDate.replace(/%s/, maxDate) - ) - - await userEvent.click(startMonth) - await userEvent.keyboard('{ArrowUp>2}') - - expect(messages()[0]).toHaveTextContent( - nb.errorRangeStartDateMaxDate.replace(/%s/, maxDate) - ) - expect(messages()[1]).toHaveTextContent( - nb.errorRangeEndDateMaxDate.replace(/%s/, maxDate) - ) - - await userEvent.click(endMonth) - await userEvent.keyboard('{ArrowDown>2}') - - expect(messages()[0]).toHaveTextContent( - nb.errorRangeStartDateMaxDate.replace(/%s/, maxDate) - ) - expect(messages()[1]).toHaveTextContent( - nb.errorRangeEndDateMinDate.replace(/%s/, minDate) - ) - - await userEvent.click(startMonth) - await userEvent.keyboard('{ArrowDown>2}') - - expect(messages()[0]).toHaveTextContent( - nb.errorRangeStartDateMinDate.replace(/%s/, minDate) - ) - expect(messages()[1]).toHaveTextContent( - nb.errorRangeEndDateMinDate.replace(/%s/, minDate) - ) - - await userEvent.click(startMonth) - await userEvent.keyboard('{ArrowUp}') - await userEvent.click(endMonth) - await userEvent.keyboard('{ArrowUp}') - - expect( - document.querySelector('.dnb-form-status--error') - ).not.toBeInTheDocument() - }) - - it('should validate date limits based on `date` prop', async () => { - const minDate = '2025-01-01' - const maxDate = '2025-01-31' - - // Test min date - const { rerender } = render( - - ) - - const day = document.querySelector( - '.dnb-date-picker__input--day' - ) as HTMLInputElement - - expect( - document.querySelector('.dnb-form-status--error') - ).toBeInTheDocument() - - expect(screen.getByRole('alert')).toHaveTextContent( - nb.errorMinDate.replace(/%s/, minDate) - ) - - await userEvent.click(day) - await userEvent.keyboard('{ArrowUp}') - - expect( - document.querySelector('.dnb-form-status--error') - ).not.toBeInTheDocument() - - // Test max date - rerender() - - expect( - document.querySelector('.dnb-form-status--error') - ).toBeInTheDocument() - - expect(screen.getByRole('alert')).toHaveTextContent( - nb.errorMaxDate.replace(/%s/, maxDate) - ) - - await userEvent.click(day) - await userEvent.keyboard('{ArrowDown}') - - expect( - document.querySelector('.dnb-form-status--error') - ).not.toBeInTheDocument() - }) - - it('should validate date limits based on `startDate` and `endDate` props', async () => { - const minDate = '2025-01-01' - const maxDate = '2025-01-31' - - render( - - ) - - const [startDay, endDay] = Array.from( - document.querySelectorAll('.dnb-date-picker__input--day') - ) as Array - - const [startDayError, endDayError] = Array.from( - document.querySelectorAll('.dnb-li') - ) as Array - - expect( - document.querySelector('.dnb-form-status--error') - ).toBeInTheDocument() - - expect(startDayError).toHaveTextContent( - nb.errorRangeStartDateMinDate.replace(/%s/, minDate) - ) - expect(endDayError).toHaveTextContent( - nb.errorRangeEndDateMaxDate.replace(/%s/, maxDate) - ) - - await userEvent.click(startDay) - await userEvent.keyboard('{ArrowUp}') - await userEvent.click(endDay) - await userEvent.keyboard('{ArrowDown}') - - expect( - document.querySelector('.dnb-form-status--error') - ).not.toBeInTheDocument() - }) - it('has to auto-correct invalid min/max dates', async () => { const onChange = jest.fn() @@ -1791,113 +1455,6 @@ describe('DatePicker component', () => { ) }) - it('should display date limit error messages based on locale', async () => { - const minDate = '2025-01-01' - const maxDate = '2025-01-31' - - const { rerender } = render( - - - - ) - - const [day, month] = Array.from( - document.querySelectorAll('.dnb-date-picker__input') - ) - - await userEvent.click(day) - await userEvent.keyboard('{ArrowDown}') - - expect( - document.querySelector('.dnb-form-status--error') - ).toBeInTheDocument() - expect(screen.getByRole('alert')).toHaveTextContent( - en.errorMinDate.replace(/%s/, minDate) - ) - - await userEvent.click(month) - await userEvent.keyboard('{ArrowUp>2}') - - expect(screen.getByRole('alert')).toHaveTextContent( - en.errorMaxDate.replace(/%s/, maxDate) - ) - - rerender( - - - - ) - - const [, startMonth, , , endMonth] = Array.from( - document.querySelectorAll('.dnb-date-picker__input') - ) as Array - - await userEvent.click(startMonth) - await userEvent.keyboard('{ArrowDown}') - await userEvent.click(endMonth) - await userEvent.keyboard('{ArrowUp}') - - expect( - document.querySelector('.dnb-form-status--error') - ).toBeInTheDocument() - - const messages = () => - Array.from( - document.querySelectorAll('.dnb-form-status--error .dnb-li') - ) as Array - - expect(screen.getByRole('alert')).toHaveTextContent(en.errorSummary) - - expect(messages()[0]).toHaveTextContent( - en.errorRangeStartDateMinDate.replace(/%s/, minDate) - ) - expect(messages()[1]).toHaveTextContent( - en.errorRangeEndDateMaxDate.replace(/%s/, maxDate) - ) - - await userEvent.click(startMonth) - await userEvent.keyboard('{ArrowUp>2}') - - expect(messages()[0]).toHaveTextContent( - en.errorRangeStartDateMaxDate.replace(/%s/, maxDate) - ) - expect(messages()[1]).toHaveTextContent( - en.errorRangeEndDateMaxDate.replace(/%s/, maxDate) - ) - - await userEvent.click(endMonth) - await userEvent.keyboard('{ArrowDown>2}') - - expect(messages()[0]).toHaveTextContent( - en.errorRangeStartDateMaxDate.replace(/%s/, maxDate) - ) - expect(messages()[1]).toHaveTextContent( - en.errorRangeEndDateMinDate.replace(/%s/, minDate) - ) - - await userEvent.click(startMonth) - await userEvent.keyboard('{ArrowDown>2}') - - expect(messages()[0]).toHaveTextContent( - en.errorRangeStartDateMinDate.replace(/%s/, minDate) - ) - expect(messages()[1]).toHaveTextContent( - en.errorRangeEndDateMinDate.replace(/%s/, minDate) - ) - }) - it('has to auto-correct invalid date based on min date', async () => { render( { rightPicker.querySelector('.dnb-date-picker__header__title') ).toHaveTextContent('november 2024') }) + + describe('DatePicker date limit validation', () => { + it('should display error message if `date` is before `minDate`', async () => { + const minDate = '2025-01-01' + + render() + + const day = document.querySelector( + '.dnb-date-picker__input--day' + ) as HTMLInputElement + + expect( + document.querySelector('.dnb-form-status--error') + ).not.toBeInTheDocument() + + await userEvent.click(day) + await userEvent.keyboard('{ArrowDown}') + + expect( + document.querySelector('.dnb-form-status--error') + ).toBeInTheDocument() + + expect(screen.getByRole('alert')).toHaveTextContent( + nb.errorMinDate.replace(/%s/, minDate) + ) + }) + + it('should display error message if `date` is after `maxDate`', async () => { + const maxDate = '2025-01-31' + + render() + + const day = document.querySelector( + '.dnb-date-picker__input--day' + ) as HTMLInputElement + + expect( + document.querySelector('.dnb-form-status--error') + ).not.toBeInTheDocument() + + await userEvent.click(day) + await userEvent.keyboard('{ArrowUp}') + + expect( + document.querySelector('.dnb-form-status--error') + ).toBeInTheDocument() + + expect(screen.getByRole('alert')).toHaveTextContent( + nb.errorMaxDate.replace(/%s/, maxDate) + ) + }) + + it('should display error message if `startDate` is before `minDate`', async () => { + const minDate = '2025-01-01' + + render( + + ) + + const [startDay] = Array.from( + document.querySelectorAll('.dnb-date-picker__input--day') + ) as Array + + expect( + document.querySelector('.dnb-form-status--error') + ).not.toBeInTheDocument() + + await userEvent.click(startDay) + await userEvent.keyboard('{ArrowDown}') + + expect( + document.querySelector('.dnb-form-status--error') + ).toBeInTheDocument() + + expect(screen.getByRole('alert')).toHaveTextContent( + nb.errorRangeStartDateMinDate.replace(/%s/, minDate) + ) + }) + + it('should display error message if `startDate` is after `maxDate`', async () => { + const maxDate = '2025-01-31' + + render( + + ) + + const [startDay] = Array.from( + document.querySelectorAll('.dnb-date-picker__input--day') + ) as Array + + expect( + document.querySelector('.dnb-form-status--error') + ).not.toBeInTheDocument() + + await userEvent.click(startDay) + await userEvent.keyboard('{ArrowUp}') + + expect( + document.querySelector('.dnb-form-status--error') + ).toBeInTheDocument() + + expect(screen.getByRole('alert')).toHaveTextContent( + nb.errorRangeStartDateMaxDate.replace(/%s/, maxDate) + ) + }) + + it('should display error message if `endDate` is before `minDate`', async () => { + const minDate = '2025-01-01' + + render( + + ) + + const [, endDay] = Array.from( + document.querySelectorAll('.dnb-date-picker__input--day') + ) as Array + + expect( + document.querySelector('.dnb-form-status--error') + ).not.toBeInTheDocument() + + await userEvent.click(endDay) + await userEvent.keyboard('{ArrowDown}') + + expect( + document.querySelector('.dnb-form-status--error') + ).toBeInTheDocument() + + expect(screen.getByRole('alert')).toHaveTextContent( + nb.errorRangeEndDateMinDate.replace(/%s/, minDate) + ) + }) + + it('should display error message if `endDate` is after `maxDate`', async () => { + const maxDate = '2025-01-31' + + render( + + ) + + const [, endDay] = Array.from( + document.querySelectorAll('.dnb-date-picker__input--day') + ) as Array + + expect( + document.querySelector('.dnb-form-status--error') + ).not.toBeInTheDocument() + + await userEvent.click(endDay) + await userEvent.keyboard('{ArrowUp}') + + expect( + document.querySelector('.dnb-form-status--error') + ).toBeInTheDocument() + + expect(screen.getByRole('alert')).toHaveTextContent( + nb.errorRangeEndDateMaxDate.replace(/%s/, maxDate) + ) + }) + + it('should display error messages if `startDate` and `endDate` date is outside of limits in `range` mode', async () => { + const minDate = '2025-01-01' + const maxDate = '2025-01-31' + + render( + + ) + + const [startDay, startMonth, , endDay, endMonth] = Array.from( + document.querySelectorAll('.dnb-date-picker__input') + ) as Array + + expect( + document.querySelector('.dnb-form-status--error') + ).not.toBeInTheDocument() + + // Focus on day inputs and select out of limit dates + await userEvent.click(startDay) + await userEvent.keyboard('{ArrowDown}') + await userEvent.click(endDay) + await userEvent.keyboard('{ArrowUp}') + + expect( + document.querySelector('.dnb-form-status--error') + ).toBeInTheDocument() + + const statusText = document.querySelector('.dnb-form-status__text') + const messages = () => + Array.from( + statusText.querySelectorAll('.dnb-li') + ) as Array + + expect(statusText).toHaveTextContent(nb.errorSummary) + + expect(messages()[0]).toHaveTextContent( + nb.errorRangeStartDateMinDate.replace(/%s/, minDate) + ) + expect(messages()[1]).toHaveTextContent( + nb.errorRangeEndDateMaxDate.replace(/%s/, maxDate) + ) + + await userEvent.click(startMonth) + await userEvent.keyboard('{ArrowUp>2}') + + expect(messages()[0]).toHaveTextContent( + nb.errorRangeStartDateMaxDate.replace(/%s/, maxDate) + ) + expect(messages()[1]).toHaveTextContent( + nb.errorRangeEndDateMaxDate.replace(/%s/, maxDate) + ) + + await userEvent.click(endMonth) + await userEvent.keyboard('{ArrowDown>2}') + + expect(messages()[0]).toHaveTextContent( + nb.errorRangeStartDateMaxDate.replace(/%s/, maxDate) + ) + expect(messages()[1]).toHaveTextContent( + nb.errorRangeEndDateMinDate.replace(/%s/, minDate) + ) + + await userEvent.click(startMonth) + await userEvent.keyboard('{ArrowDown>2}') + + expect(messages()[0]).toHaveTextContent( + nb.errorRangeStartDateMinDate.replace(/%s/, minDate) + ) + expect(messages()[1]).toHaveTextContent( + nb.errorRangeEndDateMinDate.replace(/%s/, minDate) + ) + + await userEvent.click(startMonth) + await userEvent.keyboard('{ArrowUp}') + await userEvent.click(endMonth) + await userEvent.keyboard('{ArrowUp}') + + expect( + document.querySelector('.dnb-form-status--error') + ).not.toBeInTheDocument() + }) + + it('should validate date limits based on `date` prop', async () => { + const minDate = '2025-01-01' + const maxDate = '2025-01-31' + + // Test min date + const { rerender } = render( + + ) + + const day = document.querySelector( + '.dnb-date-picker__input--day' + ) as HTMLInputElement + + expect( + document.querySelector('.dnb-form-status--error') + ).toBeInTheDocument() + + expect(screen.getByRole('alert')).toHaveTextContent( + nb.errorMinDate.replace(/%s/, minDate) + ) + + await userEvent.click(day) + await userEvent.keyboard('{ArrowUp}') + + expect( + document.querySelector('.dnb-form-status--error') + ).not.toBeInTheDocument() + + // Test max date + rerender() + + expect( + document.querySelector('.dnb-form-status--error') + ).toBeInTheDocument() + + expect(screen.getByRole('alert')).toHaveTextContent( + nb.errorMaxDate.replace(/%s/, maxDate) + ) + + await userEvent.click(day) + await userEvent.keyboard('{ArrowDown}') + + expect( + document.querySelector('.dnb-form-status--error') + ).not.toBeInTheDocument() + }) + + it('should validate date limits based on `startDate` and `endDate` props', async () => { + const minDate = '2025-01-01' + const maxDate = '2025-01-31' + + render( + + ) + + const [startDay, endDay] = Array.from( + document.querySelectorAll('.dnb-date-picker__input--day') + ) as Array + + const [startDayError, endDayError] = Array.from( + document.querySelectorAll('.dnb-li') + ) as Array + + expect( + document.querySelector('.dnb-form-status--error') + ).toBeInTheDocument() + + expect(startDayError).toHaveTextContent( + nb.errorRangeStartDateMinDate.replace(/%s/, minDate) + ) + expect(endDayError).toHaveTextContent( + nb.errorRangeEndDateMaxDate.replace(/%s/, maxDate) + ) + + await userEvent.click(startDay) + await userEvent.keyboard('{ArrowUp}') + await userEvent.click(endDay) + await userEvent.keyboard('{ArrowDown}') + + expect( + document.querySelector('.dnb-form-status--error') + ).not.toBeInTheDocument() + }) + + it('should display date limit error messages based on locale', async () => { + const minDate = '2025-01-01' + const maxDate = '2025-01-31' + + const { rerender } = render( + + + + ) + + const [day, month] = Array.from( + document.querySelectorAll('.dnb-date-picker__input') + ) + + await userEvent.click(day) + await userEvent.keyboard('{ArrowDown}') + + expect( + document.querySelector('.dnb-form-status--error') + ).toBeInTheDocument() + expect(screen.getByRole('alert')).toHaveTextContent( + en.errorMinDate.replace(/%s/, minDate) + ) + + await userEvent.click(month) + await userEvent.keyboard('{ArrowUp>2}') + + expect(screen.getByRole('alert')).toHaveTextContent( + en.errorMaxDate.replace(/%s/, maxDate) + ) + + rerender( + + + + ) + + const [, startMonth, , , endMonth] = Array.from( + document.querySelectorAll('.dnb-date-picker__input') + ) as Array + + await userEvent.click(startMonth) + await userEvent.keyboard('{ArrowDown}') + await userEvent.click(endMonth) + await userEvent.keyboard('{ArrowUp}') + + expect( + document.querySelector('.dnb-form-status--error') + ).toBeInTheDocument() + + const messages = () => + Array.from( + document.querySelectorAll('.dnb-form-status--error .dnb-li') + ) as Array + + expect(screen.getByRole('alert')).toHaveTextContent(en.errorSummary) + + expect(messages()[0]).toHaveTextContent( + en.errorRangeStartDateMinDate.replace(/%s/, minDate) + ) + expect(messages()[1]).toHaveTextContent( + en.errorRangeEndDateMaxDate.replace(/%s/, maxDate) + ) + + await userEvent.click(startMonth) + await userEvent.keyboard('{ArrowUp>2}') + + expect(messages()[0]).toHaveTextContent( + en.errorRangeStartDateMaxDate.replace(/%s/, maxDate) + ) + expect(messages()[1]).toHaveTextContent( + en.errorRangeEndDateMaxDate.replace(/%s/, maxDate) + ) + + await userEvent.click(endMonth) + await userEvent.keyboard('{ArrowDown>2}') + + expect(messages()[0]).toHaveTextContent( + en.errorRangeStartDateMaxDate.replace(/%s/, maxDate) + ) + expect(messages()[1]).toHaveTextContent( + en.errorRangeEndDateMinDate.replace(/%s/, minDate) + ) + + await userEvent.click(startMonth) + await userEvent.keyboard('{ArrowDown>2}') + + expect(messages()[0]).toHaveTextContent( + en.errorRangeStartDateMinDate.replace(/%s/, minDate) + ) + expect(messages()[1]).toHaveTextContent( + en.errorRangeEndDateMinDate.replace(/%s/, minDate) + ) + }) + }) }) describe('DatePicker scss', () => { From a61749365f8cbbf702cfbfbf2a27b175ba79baf9 Mon Sep 17 00:00:00 2001 From: Joakim Bjerknes Date: Wed, 22 Jan 2025 13:19:20 +0100 Subject: [PATCH 14/24] add examples to docs --- .../uilib/components/date-picker/Examples.tsx | 17 +++++++++++++++++ .../docs/uilib/components/date-picker/demos.mdx | 7 +++++++ 2 files changed, 24 insertions(+) diff --git a/packages/dnb-design-system-portal/src/docs/uilib/components/date-picker/Examples.tsx b/packages/dnb-design-system-portal/src/docs/uilib/components/date-picker/Examples.tsx index 62644fe95c3..f89ebb33bec 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/components/date-picker/Examples.tsx +++ b/packages/dnb-design-system-portal/src/docs/uilib/components/date-picker/Examples.tsx @@ -373,3 +373,20 @@ export const DatePickerCorrectInvalidDate = () => ( /> ) + +export const DatePickerDateLimitValidation = () => { + return ( + + + + + + ) +} diff --git a/packages/dnb-design-system-portal/src/docs/uilib/components/date-picker/demos.mdx b/packages/dnb-design-system-portal/src/docs/uilib/components/date-picker/demos.mdx index e7fbbd9dc7d..5a58dbe5563 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/components/date-picker/demos.mdx +++ b/packages/dnb-design-system-portal/src/docs/uilib/components/date-picker/demos.mdx @@ -18,6 +18,7 @@ import { DatePickerScreenshotTestSizes, DatePickerScreenshotTestDisabled, DatePickerCorrectInvalidDate, + DatePickerDateLimitValidation, } from 'Docs/uilib/components/date-picker/Examples' import ChangeLocale from 'dnb-design-system-portal/src/core/ChangeLocale' @@ -98,6 +99,12 @@ import enUS from '@dnb/eufemia/shared/locales/en-US' +### DatePicker with date limit validation + +The DatePicker will automatically display an error message if the selected date or dates are outside the given date limits. + + + ### Opened DatePicker From 7c09b9fbacbdad3a4193a377a17c50d4ec0ba2df Mon Sep 17 00:00:00 2001 From: Joakim Bjerknes Date: Wed, 22 Jan 2025 14:00:18 +0100 Subject: [PATCH 15/24] update date error message date formatting --- .../hooks/useDateLimitValidation.tsx | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/packages/dnb-eufemia/src/components/date-picker/hooks/useDateLimitValidation.tsx b/packages/dnb-eufemia/src/components/date-picker/hooks/useDateLimitValidation.tsx index b276a7a28cb..ed450e97276 100644 --- a/packages/dnb-eufemia/src/components/date-picker/hooks/useDateLimitValidation.tsx +++ b/packages/dnb-eufemia/src/components/date-picker/hooks/useDateLimitValidation.tsx @@ -4,27 +4,36 @@ import { DatePickerDates } from './useDates' import { format, isAfter, isBefore } from 'date-fns' import { DatePickerProps } from '../DatePicker' import { Li, Ul } from '../../../elements' +import { enGB, nb } from 'date-fns/locale' +import { ProviderProps } from '../../../shared/Provider' type DateLimitValidation = { status: DatePickerProps['status'] statusState: DatePickerProps['statusState'] } +const locales = { + 'nb-NO': nb, + 'en-GB': enGB, +} + export default function useDateLimitValidation({ minDate, maxDate, endDate, startDate, isRange, - dateFormat, + localeKey = 'nbNO', }: Pick< DatePickerDates, 'startDate' | 'endDate' | 'minDate' | 'maxDate' > & { isRange: DatePickerProps['range'] - dateFormat: DatePickerProps['dateFormat'] + localeKey: ProviderProps['locale'] }) { const translation = useTranslation().DatePicker + const dateFormat = 'PPP' + const locale = locales[localeKey] const validationMessage = useMemo< DateLimitValidation | undefined @@ -41,7 +50,7 @@ export default function useDateLimitValidation({ return { status: translation.errorMinDate.replace( /%s/, - format(minDate, dateFormat) + format(minDate, dateFormat, { locale }) ), statusState, } @@ -51,7 +60,7 @@ export default function useDateLimitValidation({ return { status: translation.errorMaxDate.replace( /%s/, - format(maxDate, dateFormat) + format(maxDate, dateFormat, { locale }) ), statusState, } @@ -64,7 +73,7 @@ export default function useDateLimitValidation({ messages.push( translation.errorRangeStartDateMinDate.replace( /%s/, - format(minDate, dateFormat) + format(minDate, dateFormat, { locale }) ) ) } @@ -73,7 +82,7 @@ export default function useDateLimitValidation({ messages.push( translation.errorRangeStartDateMaxDate.replace( /%s/, - format(maxDate, dateFormat) + format(maxDate, dateFormat, { locale }) ) ) } @@ -82,7 +91,7 @@ export default function useDateLimitValidation({ messages.push( translation.errorRangeEndDateMinDate.replace( /%s/, - format(minDate, dateFormat) + format(minDate, dateFormat, { locale }) ) ) } @@ -91,7 +100,7 @@ export default function useDateLimitValidation({ messages.push( translation.errorRangeEndDateMaxDate.replace( /%s/, - format(maxDate, dateFormat) + format(maxDate, dateFormat, { locale }) ) ) } @@ -102,15 +111,7 @@ export default function useDateLimitValidation({ : messages[0] return status ? { status, statusState } : undefined - }, [ - startDate, - endDate, - minDate, - maxDate, - isRange, - dateFormat, - translation, - ]) + }, [startDate, endDate, minDate, maxDate, isRange, translation, locale]) return validationMessage } From 92fa4b2dc4a9a840b14352bdd297fdcee1fd5f65 Mon Sep 17 00:00:00 2001 From: Joakim Bjerknes Date: Wed, 22 Jan 2025 14:16:19 +0100 Subject: [PATCH 16/24] revert DatePickerProvider --- .../date-picker/DatePickerProvider.tsx | 41 ++++++++++++++----- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/packages/dnb-eufemia/src/components/date-picker/DatePickerProvider.tsx b/packages/dnb-eufemia/src/components/date-picker/DatePickerProvider.tsx index 5ce56046cba..7732952a302 100644 --- a/packages/dnb-eufemia/src/components/date-picker/DatePickerProvider.tsx +++ b/packages/dnb-eufemia/src/components/date-picker/DatePickerProvider.tsx @@ -20,7 +20,7 @@ import DatePickerContext, { DatePickerContextValues, } from './DatePickerContext' import useViews, { CalendarView } from './hooks/useViews' -import { DatePickerDateProps, DatePickerDates } from './hooks/useDates' +import useDates, { DatePickerDates } from './hooks/useDates' import useLastEventCallCache, { LastEventCallCache, } from './hooks/useLastEventCallCache' @@ -32,10 +32,6 @@ type DatePickerProviderProps = DatePickerAllProps & { hidePicker?: DatePickerContextValues['hidePicker'] attributes?: DatePickerEventAttributes children: React.ReactNode - dates: DatePickerDates - previousDateProps: DatePickerDateProps - updateDates: (dates: DatePickerDates) => void - hasHadValidDate: boolean } export type DatePickerChangeEvent = DatePickerDates & { @@ -75,11 +71,16 @@ function DatePickerProvider(externalProps: DatePickerProviderProps) { const props = { ...defaultProps, ...externalProps } const { - dates, - updateDates, - hasHadValidDate, - previousDateProps, + date, + startDate, + endDate, + startMonth, + endMonth, + minDate, + maxDate, + dateFormat, range, + correctInvalidDate, attributes, returnFormat: returnFormatProp, children, @@ -90,6 +91,24 @@ function DatePickerProvider(externalProps: DatePickerProviderProps) { const sharedContext = useContext(SharedContext) + const { dates, updateDates, hasHadValidDate, previousDateProps } = + useDates( + { + date, + startDate, + endDate, + startMonth, + endMonth, + minDate, + maxDate, + }, + { + dateFormat: dateFormat, + isRange: range, + shouldCorrectDate: correctInvalidDate, + } + ) + const { views, setViews, forceViewMonthChange } = useViews({ startMonth: dates.startMonth, endMonth: dates.endMonth, @@ -113,7 +132,7 @@ function DatePickerProvider(externalProps: DatePickerProviderProps) { const returnFormat = correctV1Format(returnFormatProp) const startDateIsValid = Boolean(startDate && isValid(startDate)) const endDateIsValid = Boolean(endDate && isValid(endDate)) - const hasMinOrMaxDates = dates.minDate || dates.maxDate + const hasMinOrMaxDates = minDate || maxDate const returnObject: ReturnObject = { event, @@ -160,7 +179,7 @@ function DatePickerProvider(externalProps: DatePickerProviderProps) { : startDateIsValid, } }, - [dates, views, attributes, range, returnFormatProp] + [dates, views, attributes, maxDate, minDate, range, returnFormatProp] ) const callOnChangeHandler = useCallback( From efe289656d9dd2e8163ab5ac1f3332a97b3f1df4 Mon Sep 17 00:00:00 2001 From: Joakim Bjerknes Date: Wed, 22 Jan 2025 14:18:02 +0100 Subject: [PATCH 17/24] revert FormStatus types --- .../dnb-eufemia/src/components/form-status/FormStatus.d.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/dnb-eufemia/src/components/form-status/FormStatus.d.ts b/packages/dnb-eufemia/src/components/form-status/FormStatus.d.ts index 201b1cdffdd..185ee1c898a 100644 --- a/packages/dnb-eufemia/src/components/form-status/FormStatus.d.ts +++ b/packages/dnb-eufemia/src/components/form-status/FormStatus.d.ts @@ -3,7 +3,11 @@ import type { GlobalStatusConfigObject } from '../GlobalStatus'; import type { IconIcon, IconSize } from '../Icon'; import type { SkeletonShow } from '../Skeleton'; import type { SpacingProps, SpaceTypeAll } from '../space/types'; -export type FormStatusText = React.ReactNode; +export type FormStatusText = + | string + | boolean + | ((...args: any[]) => any) + | React.ReactNode; export type FormStatusState = | boolean | string From 2043d3cdc0ea112397080f499faa0df1ec700770 Mon Sep 17 00:00:00 2001 From: Joakim Bjerknes Date: Wed, 22 Jan 2025 14:18:36 +0100 Subject: [PATCH 18/24] remove console.log --- .../dnb-eufemia/src/components/date-picker/DatePickerInput.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dnb-eufemia/src/components/date-picker/DatePickerInput.tsx b/packages/dnb-eufemia/src/components/date-picker/DatePickerInput.tsx index f7a8e33a456..85af3f7f050 100644 --- a/packages/dnb-eufemia/src/components/date-picker/DatePickerInput.tsx +++ b/packages/dnb-eufemia/src/components/date-picker/DatePickerInput.tsx @@ -308,7 +308,7 @@ function DatePickerInput(externalProps: DatePickerInputProps) { break } } - console.log('possibleFormats', possibleFormats) + const mode = focusMode.current === 'start' ? 'startDate' : 'endDate' From bdcc8745ad7485c98167105d13522cd18ca5e9de Mon Sep 17 00:00:00 2001 From: Joakim Bjerknes Date: Wed, 22 Jan 2025 14:25:55 +0100 Subject: [PATCH 19/24] move translations to eufemia forms --- .../src/extensions/forms/constants/locales/en-GB.ts | 6 ++++++ .../src/extensions/forms/constants/locales/nb-NO.ts | 6 ++++++ packages/dnb-eufemia/src/shared/locales/en-GB.ts | 7 ------- packages/dnb-eufemia/src/shared/locales/nb-NO.ts | 7 ------- 4 files changed, 12 insertions(+), 14 deletions(-) diff --git a/packages/dnb-eufemia/src/extensions/forms/constants/locales/en-GB.ts b/packages/dnb-eufemia/src/extensions/forms/constants/locales/en-GB.ts index af29b349039..1148f73bb94 100644 --- a/packages/dnb-eufemia/src/extensions/forms/constants/locales/en-GB.ts +++ b/packages/dnb-eufemia/src/extensions/forms/constants/locales/en-GB.ts @@ -106,6 +106,12 @@ export default { Date: { label: 'Date', errorRequired: 'You must provide a valid date.', + errorMinDate: 'Chosen date must be after %s.', + errorMaxDate: 'Chosen date must be before %s.', + errorRangeStartDateMinDate: 'Chosen start date must be after %s.', + errorRangeStartDateMaxDate: 'Chosen start date must be before %s.', + errorRangeEndDateMinDate: 'Chosen end date must be after %s.', + errorRangeEndDateMaxDate: 'Chosen end date must be before %s.', }, Expiry: { label: 'Expiry date', diff --git a/packages/dnb-eufemia/src/extensions/forms/constants/locales/nb-NO.ts b/packages/dnb-eufemia/src/extensions/forms/constants/locales/nb-NO.ts index 9bbfe860782..217d0f27aa7 100644 --- a/packages/dnb-eufemia/src/extensions/forms/constants/locales/nb-NO.ts +++ b/packages/dnb-eufemia/src/extensions/forms/constants/locales/nb-NO.ts @@ -105,6 +105,12 @@ export default { Date: { label: 'Dato', errorRequired: 'Du må angi en gyldig dato.', + errorMinDate: 'Valgt dato må være etter %s.', + errorMaxDate: 'Valgt dato må være før %s.', + errorRangeStartDateMinDate: 'Valgt startdato må være etter %s.', + errorRangeStartDateMaxDate: 'Valgt startdato må være før %s.', + errorRangeEndDateMinDate: 'Valgt sluttdato må være etter %s.', + errorRangeEndDateMaxDate: 'Valgt sluttdato må være før %s.', }, Expiry: { label: 'Utløpsdato', diff --git a/packages/dnb-eufemia/src/shared/locales/en-GB.ts b/packages/dnb-eufemia/src/shared/locales/en-GB.ts index dd325690632..bee90d107ec 100644 --- a/packages/dnb-eufemia/src/shared/locales/en-GB.ts +++ b/packages/dnb-eufemia/src/shared/locales/en-GB.ts @@ -38,13 +38,6 @@ export default { submitButtonText: 'OK', cancelButtonText: 'Cancel', resetButtonText: 'Reset', - errorSummary: 'Please correct the following errors:', - errorMinDate: 'Chosen date must be after %s', - errorMaxDate: 'Chosen date must be before %s', - errorRangeStartDateMinDate: 'Chosen start date must be after %s', - errorRangeStartDateMaxDate: 'Chosen start date must be before %s', - errorRangeEndDateMinDate: 'Chosen end date must be after %s', - errorRangeEndDateMaxDate: 'Chosen end date must be before %s', placeholderCharacters: { day: 'd', month: 'm', diff --git a/packages/dnb-eufemia/src/shared/locales/nb-NO.ts b/packages/dnb-eufemia/src/shared/locales/nb-NO.ts index b9b223371ac..1309eb93d70 100644 --- a/packages/dnb-eufemia/src/shared/locales/nb-NO.ts +++ b/packages/dnb-eufemia/src/shared/locales/nb-NO.ts @@ -37,13 +37,6 @@ export default { submitButtonText: 'Ok', cancelButtonText: 'Avbryt', resetButtonText: 'Tilbakestill', - errorSummary: 'Feil som må rettes:', - errorMinDate: 'Valgt dato må være etter %s', - errorMaxDate: 'Valgt dato må være før %s', - errorRangeStartDateMinDate: 'Valgt startdato må være etter %s', - errorRangeStartDateMaxDate: 'Valgt startdato må være før %s', - errorRangeEndDateMinDate: 'Valgt sluttdato må være etter %s', - errorRangeEndDateMaxDate: 'Valgt sluttdato må være før %s', placeholderCharacters: { day: 'd', month: 'm', From d09d02e7b788bef232aadb32e250bc32cac3bbca Mon Sep 17 00:00:00 2001 From: Joakim Bjerknes Date: Thu, 23 Jan 2025 16:05:12 +0100 Subject: [PATCH 20/24] add date limit validation to date --- .../src/extensions/forms/Field/Date/Date.tsx | 200 ++++++++++++++++-- 1 file changed, 184 insertions(+), 16 deletions(-) diff --git a/packages/dnb-eufemia/src/extensions/forms/Field/Date/Date.tsx b/packages/dnb-eufemia/src/extensions/forms/Field/Date/Date.tsx index 1e70692572c..70ee72ff995 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Field/Date/Date.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Field/Date/Date.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useContext, useMemo } from 'react' +import React, { useCallback, useContext, useMemo, useState } from 'react' import { DatePicker } from '../../../../components' import { useFieldProps } from '../../hooks' import { FieldProps, AllJSONSchemaVersions } from '../../types' @@ -6,17 +6,23 @@ import { pickSpacingProps } from '../../../../components/flex/utils' import classnames from 'classnames' import FieldBlock, { Props as FieldBlockProps } from '../../FieldBlock' import SharedContext from '../../../../shared/Context' -import { parseISO, isValid } from 'date-fns' +import { parseISO, isValid, isBefore, isAfter, format } from 'date-fns' import useTranslation from '../../hooks/useTranslation' import { formatDate } from '../../Value/Date' import { DatePickerEvent, DatePickerProps, } from '../../../../components/DatePicker' +import nb from 'date-fns/locale/nb' +import enGB from 'date-fns/locale/en-GB' +import { convertStringToDate } from '../../../../components/date-picker/DatePickerCalc' +import locales from '../../constants/locales' +import { ProviderProps } from '../../../../shared/Provider' +import { FormError } from '../../utils' // `range`, `showInput`, `showCancelButton` and `showResetButton` are not picked from the `DatePickerProps` // Since they require `Field.Date` specific comments, due to them having different default values -export type Props = FieldProps & { +export type DateProps = FieldProps & { // Validation pattern?: string /** @@ -76,17 +82,40 @@ export type Props = FieldProps & { | 'onReset' > -function DateComponent(props: Props) { - const translations = useTranslation() +function DateComponent(props: DateProps) { + const translations = useTranslation().Date const { locale } = useContext(SharedContext) + const [dateLimitErrors, setDateLimitErrors] = useState>( + validateDateLimit({ + value: props.value, + minDate: props.minDate, + maxDate: props.maxDate, + isRange: props.range, + localeKey: locale, + translations, + }) + ) + + const error = useMemo(() => { + const errors: Array = [...dateLimitErrors] + + if (Array.isArray(props.error)) { + errors.push(...props.error) + } else if (props.error) { + errors.push(props.error as Error | FormError) + } + + return errors + }, [dateLimitErrors, props.error]) + const errorMessages = useMemo(() => { return { - 'Field.errorRequired': translations.Date.errorRequired, - 'Field.errorPattern': translations.Date.errorRequired, + 'Field.errorRequired': translations.errorRequired, + 'Field.errorPattern': translations.errorRequired, ...props.errorMessages, } - }, [props.errorMessages, translations.Date.errorRequired]) + }, [props.errorMessages, translations.errorRequired]) const schema = useMemo( () => @@ -108,10 +137,11 @@ function DateComponent(props: Props) { [] ) - const preparedProps: Props = { + const preparedProps: DateProps = { ...props, errorMessages, schema, + error, fromInput: ({ date, start_date, @@ -120,6 +150,21 @@ function DateComponent(props: Props) { return range ? `${start_date}|${end_date}` : date }, validateRequired, + onBlurValidator: (value) => { + // Ideally onBlurValidator should allow for return an array of Error objects + // (makes the message easier to read for humans) + // So hopefully this as a temporary solution + setDateLimitErrors( + validateDateLimit({ + value: value, + minDate: props.minDate, + maxDate: props.maxDate, + isRange: range, + localeKey: locale, + translations, + }) + ) + }, } const { @@ -140,6 +185,8 @@ function DateComponent(props: Props) { showResetButton = true, showInput = true, onReset, + minDate, + maxDate, ...rest } = useFieldProps(preparedProps) @@ -155,13 +202,12 @@ function DateComponent(props: Props) { } } - const [startDate, endDate] = valueProp - .split('|') - // Assign to null if falsy value, to properly clear input values - .map((value) => (/(undefined|null)/.test(value) ? null : value)) + const [startDate, endDate] = parseRangeValue({ + value: valueProp, + }) return { - value: undefined, + date: undefined, startDate, endDate, } @@ -175,7 +221,7 @@ function DateComponent(props: Props) { const fieldBlockProps: FieldBlockProps = { forId: id, - label: label ?? translations.Date.label, + label: label ?? translations.label, className: classnames('dnb-forms-field-string', className), ...pickSpacingProps(props), } @@ -191,6 +237,8 @@ function DateComponent(props: Props) { showResetButton={showResetButton} startDate={startDate} endDate={endDate} + minDate={minDate} + maxDate={maxDate} status={hasError ? 'error' : undefined} range={range} onChange={handleChange} @@ -207,6 +255,126 @@ function DateComponent(props: Props) { ) } +function parseRangeValue({ value }: { value: DateProps['value'] }) { + return ( + value + .split('|') + // Assign to null if falsy value, to properly clear input values + .map((value) => (/(undefined|null)/.test(value) ? null : value)) + ) +} + +const dateFnslocales = { + 'nb-NO': nb, + 'en-GB': enGB, +} + +function validateDateLimit({ + value, + isRange, + localeKey, + translations, + ...params +}: { + value: DateProps['value'] + minDate: DateProps['minDate'] + maxDate: DateProps['maxDate'] + isRange: DateProps['range'] + localeKey: ProviderProps['locale'] + translations: (typeof locales)['nb-NO']['Date'] +}) { + if (!params.minDate && !params.maxDate) { + return [] + } + + const [startDateParsed, endDateParsed] = parseRangeValue({ + value, + }) + + const minDate = convertStringToDate(params.minDate) + const maxDate = convertStringToDate(params.maxDate) + const startDate = convertStringToDate(startDateParsed) + const endDate = convertStringToDate(endDateParsed) + + const locale = dateFnslocales[localeKey] + const dateFormat = 'PPP' + + // Handle non range validation + if (!isRange) { + if (isBefore(startDate, minDate)) { + return [ + new Error( + translations.errorMinDate.replace( + /%s/, + format(minDate, dateFormat, { locale }) + ) + ), + ] + } + + if (isAfter(startDate, maxDate)) { + return [ + new Error( + translations.errorMaxDate.replace( + /%s/, + format(maxDate, dateFormat, { locale }) + ) + ), + ] + } + + return [] + } + + const messages: Array = [] + + if (isBefore(startDate, minDate)) { + messages.push( + new Error( + translations.errorRangeStartDateMinDate.replace( + /%s/, + format(minDate, dateFormat, { locale }) + ) + ) + ) + } + + if (isAfter(startDate, maxDate)) { + messages.push( + new Error( + translations.errorRangeStartDateMaxDate.replace( + /%s/, + format(maxDate, dateFormat, { locale }) + ) + ) + ) + } + + if (isBefore(endDate, minDate)) { + messages.push( + new Error( + translations.errorRangeEndDateMinDate.replace( + /%s/, + format(minDate, dateFormat, { locale }) + ) + ) + ) + } + + if (isAfter(endDate, maxDate)) { + messages.push( + new Error( + translations.errorRangeEndDateMaxDate.replace( + /%s/, + format(maxDate, dateFormat, { locale }) + ) + ) + ) + } + + return messages +} + // Used to filter out DatePickerProps from the FieldProps. // Includes DatePickerProps that are not destructured in useFieldProps const datePickerPropKeys = [ @@ -248,7 +416,7 @@ const datePickerPropKeys = [ 'onReset', ] -function pickDatePickerProps(props: Props) { +function pickDatePickerProps(props: DateProps) { const datePickerProps = Object.keys(props).reduce( (datePickerProps, key) => { if (datePickerPropKeys.includes(key)) { From adcadec9d0e35f089d8b5fe2bbf677e300447a38 Mon Sep 17 00:00:00 2001 From: Joakim Bjerknes Date: Thu, 23 Jan 2025 16:05:45 +0100 Subject: [PATCH 21/24] remove date limit validation hook --- .../hooks/useDateLimitValidation.tsx | 133 ------------------ 1 file changed, 133 deletions(-) delete mode 100644 packages/dnb-eufemia/src/components/date-picker/hooks/useDateLimitValidation.tsx diff --git a/packages/dnb-eufemia/src/components/date-picker/hooks/useDateLimitValidation.tsx b/packages/dnb-eufemia/src/components/date-picker/hooks/useDateLimitValidation.tsx deleted file mode 100644 index ed450e97276..00000000000 --- a/packages/dnb-eufemia/src/components/date-picker/hooks/useDateLimitValidation.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import React, { useMemo } from 'react' -import { useTranslation } from '../../../shared' -import { DatePickerDates } from './useDates' -import { format, isAfter, isBefore } from 'date-fns' -import { DatePickerProps } from '../DatePicker' -import { Li, Ul } from '../../../elements' -import { enGB, nb } from 'date-fns/locale' -import { ProviderProps } from '../../../shared/Provider' - -type DateLimitValidation = { - status: DatePickerProps['status'] - statusState: DatePickerProps['statusState'] -} - -const locales = { - 'nb-NO': nb, - 'en-GB': enGB, -} - -export default function useDateLimitValidation({ - minDate, - maxDate, - endDate, - startDate, - isRange, - localeKey = 'nbNO', -}: Pick< - DatePickerDates, - 'startDate' | 'endDate' | 'minDate' | 'maxDate' -> & { - isRange: DatePickerProps['range'] - localeKey: ProviderProps['locale'] -}) { - const translation = useTranslation().DatePicker - const dateFormat = 'PPP' - const locale = locales[localeKey] - - const validationMessage = useMemo< - DateLimitValidation | undefined - >(() => { - if (!minDate && !maxDate) { - return undefined - } - - const statusState = 'error' - - // Handle non range validation - if (!isRange) { - if (isBefore(startDate, minDate)) { - return { - status: translation.errorMinDate.replace( - /%s/, - format(minDate, dateFormat, { locale }) - ), - statusState, - } - } - - if (isAfter(startDate, maxDate)) { - return { - status: translation.errorMaxDate.replace( - /%s/, - format(maxDate, dateFormat, { locale }) - ), - statusState, - } - } - } - - const messages = [] - - if (isBefore(startDate, minDate)) { - messages.push( - translation.errorRangeStartDateMinDate.replace( - /%s/, - format(minDate, dateFormat, { locale }) - ) - ) - } - - if (isAfter(startDate, maxDate)) { - messages.push( - translation.errorRangeStartDateMaxDate.replace( - /%s/, - format(maxDate, dateFormat, { locale }) - ) - ) - } - - if (isBefore(endDate, minDate)) { - messages.push( - translation.errorRangeEndDateMinDate.replace( - /%s/, - format(minDate, dateFormat, { locale }) - ) - ) - } - - if (isAfter(endDate, maxDate)) { - messages.push( - translation.errorRangeEndDateMaxDate.replace( - /%s/, - format(maxDate, dateFormat, { locale }) - ) - ) - } - - const status = - messages.length > 1 - ? combineErrorMessages(translation.errorSummary, messages) - : messages[0] - - return status ? { status, statusState } : undefined - }, [startDate, endDate, minDate, maxDate, isRange, translation, locale]) - - return validationMessage -} - -export function combineErrorMessages( - title: string, - messages: React.ReactNode[] -) { - return ( - <> - {title} -
    - {messages.map((status, i) => { - return
  • {status}
  • - })} -
- - ) -} From 38be90bb47325685c89e9713cdbcfbca7c4b796c Mon Sep 17 00:00:00 2001 From: Joakim Bjerknes Date: Thu, 23 Jan 2025 16:16:04 +0100 Subject: [PATCH 22/24] update error message text --- .../src/extensions/forms/constants/locales/en-GB.ts | 8 ++++---- .../src/extensions/forms/constants/locales/nb-NO.ts | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/dnb-eufemia/src/extensions/forms/constants/locales/en-GB.ts b/packages/dnb-eufemia/src/extensions/forms/constants/locales/en-GB.ts index 1148f73bb94..00ad9eb4fda 100644 --- a/packages/dnb-eufemia/src/extensions/forms/constants/locales/en-GB.ts +++ b/packages/dnb-eufemia/src/extensions/forms/constants/locales/en-GB.ts @@ -108,10 +108,10 @@ export default { errorRequired: 'You must provide a valid date.', errorMinDate: 'Chosen date must be after %s.', errorMaxDate: 'Chosen date must be before %s.', - errorRangeStartDateMinDate: 'Chosen start date must be after %s.', - errorRangeStartDateMaxDate: 'Chosen start date must be before %s.', - errorRangeEndDateMinDate: 'Chosen end date must be after %s.', - errorRangeEndDateMaxDate: 'Chosen end date must be before %s.', + errorRangeStartDateMinDate: 'Start date must be after %s.', + errorRangeStartDateMaxDate: 'Start date must be before %s.', + errorRangeEndDateMinDate: 'End date must be after %s.', + errorRangeEndDateMaxDate: 'End date must be before %s.', }, Expiry: { label: 'Expiry date', diff --git a/packages/dnb-eufemia/src/extensions/forms/constants/locales/nb-NO.ts b/packages/dnb-eufemia/src/extensions/forms/constants/locales/nb-NO.ts index 217d0f27aa7..510fb1888f6 100644 --- a/packages/dnb-eufemia/src/extensions/forms/constants/locales/nb-NO.ts +++ b/packages/dnb-eufemia/src/extensions/forms/constants/locales/nb-NO.ts @@ -107,10 +107,10 @@ export default { errorRequired: 'Du må angi en gyldig dato.', errorMinDate: 'Valgt dato må være etter %s.', errorMaxDate: 'Valgt dato må være før %s.', - errorRangeStartDateMinDate: 'Valgt startdato må være etter %s.', - errorRangeStartDateMaxDate: 'Valgt startdato må være før %s.', - errorRangeEndDateMinDate: 'Valgt sluttdato må være etter %s.', - errorRangeEndDateMaxDate: 'Valgt sluttdato må være før %s.', + errorRangeStartDateMinDate: 'Startdato må være etter %s.', + errorRangeStartDateMaxDate: 'Startdato må være før %s.', + errorRangeEndDateMinDate: 'Sluttdato må være etter %s.', + errorRangeEndDateMaxDate: 'Sluttdato må være før %s.', }, Expiry: { label: 'Utløpsdato', From c564373b34b7b90bb14088b9d8f5440c3c9a597e Mon Sep 17 00:00:00 2001 From: Joakim Bjerknes Date: Thu, 23 Jan 2025 16:17:24 +0100 Subject: [PATCH 23/24] move doc examples from DatePicker to Date --- .../uilib/components/date-picker/Examples.tsx | 17 ----------------- .../docs/uilib/components/date-picker/demos.mdx | 6 ------ .../forms/feature-fields/Date/Examples.tsx | 16 ++++++++++++++++ .../forms/feature-fields/Date/demos.mdx | 6 ++++++ 4 files changed, 22 insertions(+), 23 deletions(-) diff --git a/packages/dnb-design-system-portal/src/docs/uilib/components/date-picker/Examples.tsx b/packages/dnb-design-system-portal/src/docs/uilib/components/date-picker/Examples.tsx index f89ebb33bec..62644fe95c3 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/components/date-picker/Examples.tsx +++ b/packages/dnb-design-system-portal/src/docs/uilib/components/date-picker/Examples.tsx @@ -373,20 +373,3 @@ export const DatePickerCorrectInvalidDate = () => ( /> ) - -export const DatePickerDateLimitValidation = () => { - return ( - - - - - - ) -} diff --git a/packages/dnb-design-system-portal/src/docs/uilib/components/date-picker/demos.mdx b/packages/dnb-design-system-portal/src/docs/uilib/components/date-picker/demos.mdx index 5a58dbe5563..dde6ca3aabc 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/components/date-picker/demos.mdx +++ b/packages/dnb-design-system-portal/src/docs/uilib/components/date-picker/demos.mdx @@ -99,12 +99,6 @@ import enUS from '@dnb/eufemia/shared/locales/en-US' -### DatePicker with date limit validation - -The DatePicker will automatically display an error message if the selected date or dates are outside the given date limits. - - - ### Opened DatePicker diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/feature-fields/Date/Examples.tsx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/feature-fields/Date/Examples.tsx index b41277a184c..0cb3ed7027f 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/feature-fields/Date/Examples.tsx +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/feature-fields/Date/Examples.tsx @@ -1,3 +1,4 @@ +import Provider from '@dnb/eufemia/src/shared/Provider' import ComponentBox from '../../../../../../shared/tags/ComponentBox' import { Field } from '@dnb/eufemia/src/extensions/forms' @@ -98,3 +99,18 @@ export const AutoClose = () => { ) } + +export const DatePickerDateLimitValidation = () => { + return ( + + + + + + ) +} diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/feature-fields/Date/demos.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/feature-fields/Date/demos.mdx index 058224450f0..c8fd6ffa517 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/feature-fields/Date/demos.mdx +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/feature-fields/Date/demos.mdx @@ -38,6 +38,12 @@ To enable the picker to close automatically, you have to set `showCancelButton` +### Date limit validation + +The Date field will automatically display an error message if the selected date or dates are outside the given date limits. + + + ### Validation - Required From 1bbe9b2c9339aeb37c6023ffc4432e577c25957c Mon Sep 17 00:00:00 2001 From: Joakim Bjerknes Date: Thu, 23 Jan 2025 18:19:01 +0100 Subject: [PATCH 24/24] remove unused example import --- .../src/docs/uilib/components/date-picker/demos.mdx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/dnb-design-system-portal/src/docs/uilib/components/date-picker/demos.mdx b/packages/dnb-design-system-portal/src/docs/uilib/components/date-picker/demos.mdx index dde6ca3aabc..e7fbbd9dc7d 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/components/date-picker/demos.mdx +++ b/packages/dnb-design-system-portal/src/docs/uilib/components/date-picker/demos.mdx @@ -18,7 +18,6 @@ import { DatePickerScreenshotTestSizes, DatePickerScreenshotTestDisabled, DatePickerCorrectInvalidDate, - DatePickerDateLimitValidation, } from 'Docs/uilib/components/date-picker/Examples' import ChangeLocale from 'dnb-design-system-portal/src/core/ChangeLocale'