Skip to content

Commit

Permalink
fix(numeral-date): ensure internal validation uses given month and ye…
Browse files Browse the repository at this point in the history
…ar values where possible

Built in internal validations will now use `month` and `year` values if provided to validate if
`day` input value is within valid range. Adds optional dynamic message that includes the month and
days in it when a value is invalid.

fix #6438
  • Loading branch information
edleeks87 committed Jan 15, 2024
1 parent 07e0e4b commit 24648d2
Show file tree
Hide file tree
Showing 8 changed files with 360 additions and 48 deletions.
3 changes: 3 additions & 0 deletions playwright/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import * as dateLocales from "../src/locales/date-fns-locales";
export type HooksConfig = {
roundedCornersOptOut?: boolean;
focusRedesignOptOut?: boolean;
validationRedesignOptIn?: boolean;
theme?: string;
localeName?: keyof typeof dateLocales;
};
Expand Down Expand Up @@ -56,12 +57,14 @@ beforeMount<HooksConfig>(async ({ App, hooksConfig }) => {
focusRedesignOptOut,
theme = "sage",
localeName,
validationRedesignOptIn,
} = hooksConfig || {};
return (
<CarbonProvider
theme={mountedTheme(theme)}
roundedCornersOptOut={roundedCornersOptOut}
focusRedesignOptOut={focusRedesignOptOut}
validationRedesignOptIn={validationRedesignOptIn}
>
<GlobalStyle />
<I18nProvider locale={localeName ? computedLocale(localeName) : enGB}>
Expand Down
84 changes: 53 additions & 31 deletions src/components/numeral-date/numeral-date.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,6 @@ export interface FullDate extends DayMonthDate {
yyyy: string;
}

interface ValidationsObject {
dd: (datePart: string) => boolean;
mm: (datePart: string) => boolean;
yyyy: (datePart: string) => boolean;
}

export type NumeralDateObject = DayMonthDate | MonthYearDate | FullDate;

export interface NumeralDateEvent<
Expand Down Expand Up @@ -155,18 +149,59 @@ const incorrectDateFormatMessage =
"['mm', 'dd'], " +
"['mm', 'yyyy']";

const isDayValid = (day: string) => (day ? +day > 0 && +day < 32 : true);
const getMonthsForLocale = (localeName: string) => {
const year = new Date().getFullYear();
const { format } = new Intl.DateTimeFormat(localeName, { month: "long" });

return [...Array(12).keys()].map((m) => format(new Date(Date.UTC(year, m))));
};

const validationMessages = (
locale: Locale,
month?: string,
daysInMonth?: string
) => ({
dd: locale.numeralDate.validation.day(
month ? getMonthsForLocale(locale.locale())[+month - 1] : undefined,
daysInMonth
),
mm: locale.numeralDate.validation.month(),
yyyy: locale.numeralDate.validation.year(),
});

const getDaysInMonth = (month?: string, year?: string) => {
if (month && (+month > 12 || +month < 1)) {
return 31;
}
const currentDate = new Date();
const computedYear = +(year || currentDate.getFullYear());
const computedMonth = +(month || currentDate.getMonth() + 1);

// passing 0 as the third argument ensures we handle for months being 0 indexed
return new Date(computedYear, computedMonth, 0).getDate();
};

const validate = (locale: Locale, { dd, mm, yyyy }: Partial<FullDate>) => {
const failed = {
dd: "",
mm: "",
yyyy: "",
};
const daysInMonth = getDaysInMonth(mm, yyyy);

const isMonthValid = (month: string) =>
month ? +month > 0 && +month < 13 : true;
if (dd && (+dd > daysInMonth || +dd < 1)) {
failed.dd = validationMessages(locale, mm, String(daysInMonth)).dd;
}

const isYearValid = (year: string) =>
year ? +year > 1799 && +year < 2201 : true;
if (mm && (+mm > 12 || +mm < 1)) {
failed.mm = validationMessages(locale).mm;
}

if (yyyy && (+yyyy < 1800 || +yyyy > 2200)) {
failed.yyyy = validationMessages(locale).yyyy;
}

const validations: ValidationsObject = {
dd: isDayValid,
mm: isMonthValid,
yyyy: isYearValid,
return failed;
};

const getDateLabel = (datePart: string, locale: Locale) => {
Expand Down Expand Up @@ -256,12 +291,6 @@ export const NumeralDate = <DateType extends NumeralDateObject = FullDate>({
);
}, [value]);

const validationMessages = {
dd: locale.numeralDate.validation.day(),
mm: locale.numeralDate.validation.month(),
yyyy: locale.numeralDate.validation.year(),
};

const [dateValue, setDateValue] = useState<DateType>({
...((initialValue ||
(Object.fromEntries(
Expand Down Expand Up @@ -311,19 +340,14 @@ export const NumeralDate = <DateType extends NumeralDateObject = FullDate>({
}
};

const handleBlur = (datePart: keyof NumeralDateObject) => {
const handleBlur = () => {
const internalValidationEnabled =
enableInternalError || enableInternalWarning;
/* istanbul ignore else */
if (internalValidationEnabled) {
const newDatePart: string = dateValue[datePart];
const errorMessage = validations[datePart](newDatePart)
? ""
: validationMessages[datePart];

setInternalMessages((prev) => ({
...prev,
[datePart]: errorMessage,
...validate(locale, dateValue),
}));
}
setTimeout(() => {
Expand Down Expand Up @@ -462,6 +486,7 @@ export const NumeralDate = <DateType extends NumeralDateObject = FullDate>({
onChange={(e) =>
handleChange(e, datePart as keyof NumeralDateObject)
}
onBlur={handleBlur}
ref={(element) => {
refs.current[index] = element;
if (!inputRef) {
Expand All @@ -473,9 +498,6 @@ export const NumeralDate = <DateType extends NumeralDateObject = FullDate>({
inputRef.current = element;
}
}}
onBlur={() =>
handleBlur(datePart as keyof NumeralDateObject)
}
error={!!internalError}
warning={!!internalWarning}
info={!!info}
Expand Down
100 changes: 100 additions & 0 deletions src/components/numeral-date/numeral-date.pw.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,16 @@ import {
SIZE,
VALIDATION,
} from "../../../playwright/support/constants";
import { HooksConfig } from "../../../playwright";

const testData = [CHARACTERS.DIACRITICS, CHARACTERS.SPECIALCHARACTERS];
const dynamicValidations: [string, string, string, string][] = [
["02", "29", "2001", "Day in February should be a number within 1-28."],
["02", "30", "2004", "Day in February should be a number within 1-29."],
["06", "31", "2001", "Day in June should be a number within 1-30."],
["09", "31", "2001", "Day in September should be a number within 1-30."],
["12", "32", "2001", "Day in December should be a number within 1-31."],
];

test.describe("NumeralDate component", () => {
test("should render NumeralDate with data-component prop", async ({
Expand Down Expand Up @@ -306,6 +314,98 @@ test.describe("NumeralDate component", () => {
}
);

dynamicValidations.forEach(([month, day, year, validationString]) => {
test(`should display dynamic internal error message in tooltip when validationRedesignOptIn is false, month is ${month}, day is ${day} and year is ${year}`, async ({
mount,
page,
}) => {
await mount(
<NumeralDateComponent
enableInternalError
value={{ dd: "", mm: month, yyyy: year }}
/>
);

const input = numeralDateInput(page, 0);
await input.fill(day);
await input.blur();

const errorIcon = numeralDateInput(page, 2).locator("..").locator(ICON);
await expect(errorIcon).toHaveAttribute("data-element", "error");

await errorIcon.hover();
await expect(tooltipPreview(page)).toHaveText(validationString);
});
});

dynamicValidations.forEach(([month, day, year, validationString]) => {
test(`should display dynamic internal warning message in tooltip when validationRedesignOptIn is false, month is ${month}, day is ${day} and year is ${year}`, async ({
mount,
page,
}) => {
await mount(
<NumeralDateComponent
enableInternalWarning
value={{ dd: "", mm: month, yyyy: year }}
/>
);

const input = numeralDateInput(page, 0);
await input.fill(day);
await input.blur();

const warningIcon = numeralDateInput(page, 2).locator("..").locator(ICON);
await expect(warningIcon).toHaveAttribute("data-element", "warning");

await warningIcon.hover();
await expect(tooltipPreview(page)).toHaveText(validationString);
});
});

dynamicValidations.forEach(([month, day, year, validationString]) => {
test(`should display dynamic internal error message when validationRedesignOptIn is true, month is ${month}, day is ${day} and year is ${year}`, async ({
mount,
page,
}) => {
await mount<HooksConfig>(
<NumeralDateComponent
enableInternalError
value={{ dd: "", mm: month, yyyy: year }}
/>,
{ hooksConfig: { validationRedesignOptIn: true } }
);

const input = numeralDateInput(page, 0);
await input.fill(day);
await input.blur();

const errorMessage = page.getByText(validationString);
await expect(errorMessage).toBeVisible();
});
});

dynamicValidations.forEach(([month, day, year, validationString]) => {
test(`should display dynamic internal warning message when validationRedesignOptIn is true, month is ${month}, day is ${day} and year is ${year}`, async ({
mount,
page,
}) => {
await mount<HooksConfig>(
<NumeralDateComponent
enableInternalWarning
value={{ dd: "", mm: month, yyyy: year }}
/>,
{ hooksConfig: { validationRedesignOptIn: true } }
);

const input = numeralDateInput(page, 0);
await input.fill(day);
await input.blur();

const warningMessage = page.getByText(validationString);
await expect(warningMessage).toBeVisible();
});
});

([
[0, "Day should be a number within a 1-31 range.", "Day"],
[1, "Month should be a number within a 1-12 range.", "Month"],
Expand Down
Loading

0 comments on commit 24648d2

Please sign in to comment.