Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(date-input): modifying validation error containing date #3694

Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/calm-kids-visit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@nextui-org/date-input": patch
---

The Date mentioned in the Error message of the date-picker is not according to the Locale. This PR adds an layer to the validationError to ensure correct formatting date in error messages.
55 changes: 54 additions & 1 deletion packages/components/date-input/__tests__/date-input.test.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
/* eslint-disable jsx-a11y/no-autofocus */
import * as React from "react";
import {act, fireEvent, render} from "@testing-library/react";
import {CalendarDate, CalendarDateTime, DateValue, ZonedDateTime} from "@internationalized/date";
import {
CalendarDate,
CalendarDateTime,
DateValue,
ZonedDateTime,
parseDate,
} from "@internationalized/date";
import {pointerMap, triggerPress} from "@nextui-org/test-utils";
import userEvent from "@testing-library/user-event";
import {I18nProvider} from "@react-aria/i18n";

import {DateInput as DateInputBase, DateInputProps} from "../src";

Expand Down Expand Up @@ -174,6 +181,52 @@ describe("DateInput", () => {
}
}
});

it("should support error message with MinDateValue according to locale", function () {
const minValue = "2024-04-04";
const locale = "hi-IN-u-ca-indian";
let {getByRole} = render(
<I18nProvider locale={locale}>
<DateInput
defaultValue={parseDate("2024-04-03")}
label="Date"
minValue={parseDate(minValue)}
/>
</I18nProvider>,
);

let group = getByRole("group");

expect(group).toHaveAttribute("aria-describedby");

const errorComponent = document.querySelector("[data-slot=error-message]");
const localeBasedDate = new Date(minValue).toLocaleDateString(locale);

expect(errorComponent).toHaveTextContent(localeBasedDate);
});

it("should support error message with MaxDateValue according to locale", function () {
const maxValue = "2024-04-04";
const locale = "hi-IN-u-ca-indian";
let {getByRole} = render(
<I18nProvider locale={locale}>
<DateInput
defaultValue={parseDate("2024-04-05")}
label="Date"
maxValue={parseDate(maxValue)}
/>
</I18nProvider>,
);

let group = getByRole("group");

expect(group).toHaveAttribute("aria-describedby");

const errorComponent = document.querySelector("[data-slot=error-message]");
const localeBasedDate = new Date(maxValue).toLocaleDateString(locale);

expect(errorComponent).toHaveTextContent(localeBasedDate);
});
});

describe("Events", function () {
Expand Down
138 changes: 138 additions & 0 deletions packages/components/date-input/intl/messages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
export default {
"ar-AE": {
minValidationMessage: "يجب أن تكون القيمة {date} أو بعد ذلك",
maxValidationMessage: "يجب أن تكون القيمة {date} أو قبل ذلك",
},
"bg-BG": {
minValidationMessage: "Стойността трябва да бъде {date} или по-късно",
maxValidationMessage: "Стойността трябва да бъде {date} или по-рано",
},
"cs-CZ": {
minValidationMessage: "Hodnota musí být {date} nebo později",
maxValidationMessage: "Hodnota musí být {date} nebo dříve",
},
"da-DK": {
minValidationMessage: "Værdien skal være {date} eller senere",
maxValidationMessage: "Værdien skal være {date} eller tidligere",
},
"de-DE": {
minValidationMessage: "Der Wert muss {date} oder später sein",
maxValidationMessage: "Der Wert muss {date} oder früher sein",
},
"el-GR": {
minValidationMessage: "Η τιμή πρέπει να είναι {date} ή αργότερα",
maxValidationMessage: "Η τιμή πρέπει να είναι {date} ή νωρίτερα",
},
"en-US": {
minValidationMessage: "Value must be {date} or later",
maxValidationMessage: "Value must be {date} or earlier",
},
"es-ES": {
minValidationMessage: "El valor debe ser {date} o posterior",
maxValidationMessage: "El valor debe ser {date} o anterior",
},
"et-EE": {
minValidationMessage: "Väärtus peab olema {date} või hiljem",
maxValidationMessage: "Väärtus peab olema {date} või varem",
},
"fi-FI": {
minValidationMessage: "Arvon on oltava {date} tai myöhemmin",
maxValidationMessage: "Arvon on oltava {date} tai aikaisemmin",
},
"fr-FR": {
minValidationMessage: "La valeur doit être {date} ou ultérieure",
maxValidationMessage: "La valeur doit être {date} ou antérieure",
},
"he-IL": {
minValidationMessage: "הערך חייב להיות {date} או מאוחר יותר",
maxValidationMessage: "הערך חייב להיות {date} או קודם",
},
"hr-HR": {
minValidationMessage: "Vrijednost mora biti {date} ili kasnije",
maxValidationMessage: "Vrijednost mora biti {date} ili ranije",
},
"hu-HU": {
minValidationMessage: "Az értéknek {date} vagy későbbi kell lennie",
maxValidationMessage: "Az értéknek {date} vagy korábbi kell lennie",
},
"it-IT": {
minValidationMessage: "Il valore deve essere {date} o successivo",
maxValidationMessage: "Il valore deve essere {date} o precedente",
},
"ja-JP": {
minValidationMessage: "値は{date}以降でなければなりません",
maxValidationMessage: "{date}以前でなければなりません",
},
"ko-KR": {
minValidationMessage: "{date} 이후여야 합니다",
maxValidationMessage: "{date} 이전이어야 합니다",
},
"lt-LT": {
minValidationMessage: "Vertė turi būti {date} arba vėliau",
maxValidationMessage: "Vertė turi būti {date} arba ankstesnė",
},
"lv-LV": {
minValidationMessage: "Vērtībai jābūt {date} vai vēlāk",
maxValidationMessage: "Vērtībai jābūt {date} vai agrāk",
},
"nb-NO": {
minValidationMessage: "Verdien må være {date} eller senere",
maxValidationMessage: "Verdien må være {date} eller tidligere",
},
"nl-NL": {
minValidationMessage: "De waarde moet {date} of later zijn",
maxValidationMessage: "De waarde moet {date} of eerder zijn",
},
"pl-PL": {
minValidationMessage: "Wartość musi być {date} lub późniejsza",
maxValidationMessage: "Wartość musi być {date} lub wcześniejsza",
},
"pt-BR": {
minValidationMessage: "O valor deve ser {date} ou posterior",
maxValidationMessage: "O valor deve ser {date} ou anterior",
},
"pt-PT": {
minValidationMessage: "O valor deve ser {date} ou mais tarde",
maxValidationMessage: "O valor deve ser {date} ou antes",
},
"ro-RO": {
minValidationMessage: "Valoarea trebuie să fie {date} sau mai târziu",
maxValidationMessage: "Valoarea trebuie să fie {date} sau mai devreme",
},
"ru-RU": {
minValidationMessage: "Значение должно быть {date} или позже",
maxValidationMessage: "Значение должно быть {date} или раньше",
},
"sk-SK": {
minValidationMessage: "Hodnota musí byť {date} alebo neskôr",
maxValidationMessage: "Hodnota musí byť {date} alebo skôr",
},
"sl-SI": {
minValidationMessage: "Vrednost mora biti {date} ali kasneje",
maxValidationMessage: "Vrednost mora biti {date} ali prej",
},
"sr-SP": {
minValidationMessage: "Vrednost mora biti {date} ili kasnije",
maxValidationMessage: "Vrednost mora biti {date} ili ranije",
},
"sv-SE": {
minValidationMessage: "Värdet måste vara {date} eller senare",
maxValidationMessage: "Värdet måste vara {date} eller tidigare",
},
"tr-TR": {
minValidationMessage: "Değer {date} veya sonrasında olmalıdır",
maxValidationMessage: "Değer {date} veya daha önce olmalıdır",
},
"uk-UA": {
minValidationMessage: "Значення повинно бути {date} або пізніше",
maxValidationMessage: "Значення повинно бути {date} або раніше",
},
"zh-CN": {
minValidationMessage: "值必须为 {date} 或更晚",
maxValidationMessage: "值必须为 {date} 或更早",
},
"zh-TW": {
minValidationMessage: "值必須為 {date} 或更晚",
maxValidationMessage: "值必須為 {date} 或更早",
},
};
46 changes: 45 additions & 1 deletion packages/components/date-input/src/use-date-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type {ReactRef} from "@nextui-org/react-utils";
import type {DOMAttributes, GroupDOMAttributes} from "@react-types/shared";
import type {DateInputGroupProps} from "./date-input-group";

import {useLocale} from "@react-aria/i18n";
import {useLocale, useLocalizedStringFormatter} from "@react-aria/i18n";
import {createCalendar, CalendarDate, DateFormatter} from "@internationalized/date";
import {mergeProps} from "@react-aria/utils";
import {PropGetter, useProviderContext} from "@nextui-org/system";
Expand All @@ -18,6 +18,8 @@ import {objectToDeps, clsx, dataAttr, getGregorianYearOffset} from "@nextui-org/
import {dateInput, cn} from "@nextui-org/theme";
import {useMemo} from "react";

import intlMessages from "../intl/messages";

type NextUIBaseProps<T extends DateValue> = Omit<
HTMLNextUIProps<"div">,
keyof AriaDateFieldProps<T> | "onChange"
Expand Down Expand Up @@ -184,6 +186,48 @@ export function useDateInput<T extends DateValue>(originalProps: UseDateInputPro
isInvalid: ariaIsInvalid,
} = useAriaDateField({...originalProps, label, validationBehavior, inputRef}, state, domRef);

const stringFormatter = useLocalizedStringFormatter(intlMessages) as any;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think as any is not required.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in: 8ac47e4


if (props.minValue != undefined && validationDetails.rangeUnderflow) {
const indexInValidationErrors: number =
Number(validationDetails.badInput) +
Number(validationDetails.customError) +
Number(validationDetails.patternMismatch);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need to cater rangeUnderflow as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need rangeUnderflow here as only badInput, customError and patternMatch errors appears before rangeUnderflow error in validationErrors array. Hence, if some or all of them exist they will shift the index of rangeUnderflow in validationErrors.

But in case of rangeOverflow as we have badInput, customError , patternMatch and rangeUnderflow errors before it in validationErrors array, we need to consider all of them.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May I know where did you get this info?

Copy link
Contributor Author

@macci001 macci001 Sep 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I assumed that the errors in the validationField will be populated according to the occurrence of the property in ValidityState.
But seems like I made a wrong assumption, as Errors related to Bad Input are populated in validationError after rangeUnderflow and rangeOverflow by RA .(ref).

Also, while scrapping through RA code, I understood that if ValidationError has rangeUnderflow/rangeOverflow then it can only have badInput error.
Now, as we might not consider the order of errors in ValidityState as order in ValidityError, it might be bit tricky to find out which error in ValidationError list corresponds to underflow/overflow.

It is clear that if exists in ValidationError, the rangeOverflow and rangeUnderflow will be its first element. So should we edit the first value in the list?

The reason why the rangeOverflow and rangeUnderflow will always be first element:

  • ValidationErrors can either be obtained from controlledError, serverError, clientError, builtinValidation, currentValidity (ref)
  • controlledError, serverError, clientError and currentValidity => All of these does not have true for the ValidityState corresponding to rangeUnderflow, rangeOverflow and BadInput.
  • rangeOverflow / rangeUnderflow happens to be in the validation errors list if it is obtained from builtinValidation.
  • The order in which the errors are populated for builtinValidation is this.
  • Hence, the rangeOverflow / rangeUnderflow appears to be the first element if they do exist.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the following changes in 7838f48

  • Added support for badInput message with i18n.
  • Modifying the validationError according exactly how they were populated by RA

const minValueDate = new Date(
minValue.year,
minValue.month - 1,
minValue.day,
).toLocaleDateString(locale);
const timeZone =
state.segments.filter((segment) => segment.type === "timeZoneName")[0]?.text ?? "";
const rangeUnderflow = stringFormatter
.format("minValidationMessage")
.replace("{date}", `${minValueDate} ${timeZone}`);

validationErrors.splice(indexInValidationErrors, 1, rangeUnderflow);
}

if (props.maxValue != undefined && validationDetails.rangeOverflow) {
const indexInValidationErrors: number =
Number(validationDetails.badInput) +
Number(validationDetails.customError) +
Number(validationDetails.patternMismatch) +
Number(validationDetails.rangeUnderflow);
const maxValueDate = new Date(
maxValue.year,
maxValue.month - 1,
maxValue.day,
).toLocaleDateString(locale);
const timeZone =
state.segments.filter((segment) => segment.type === "timeZoneName")[0]?.text ?? "";

const rangeOverflow = stringFormatter
.format("maxValidationMessage")
.replace("{date}", `${maxValueDate} ${timeZone}`);

validationErrors.splice(indexInValidationErrors, 1, rangeOverflow);
}

const baseStyles = clsx(classNames?.base, className);

const isInvalid = isInvalidProp || ariaIsInvalid;
Expand Down