Skip to content

Commit

Permalink
fix: incorrect year in showMonthAndYearPickers with locale (#3331)
Browse files Browse the repository at this point in the history
* fix(date-input): add gregorian year offset to minValue & maxValue

* feat(shared-utils): add getGregorianYearOffset

* fix(calendar): add gregorian year offset to minValue & maxValue

* feat(changeset): add changeset

* fix(system): remove defaultDates.minDate and defaultDates.maxDate

* fix(calendar): add missing import

* feat(date-picker): add test

* feat(calendar): add test
  • Loading branch information
wingkwong authored Jul 6, 2024
1 parent fd4b720 commit f5d94f9
Show file tree
Hide file tree
Showing 8 changed files with 152 additions and 18 deletions.
8 changes: 8 additions & 0 deletions .changeset/purple-singers-knock.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@nextui-org/calendar": patch
"@nextui-org/date-input": patch
"@nextui-org/system": patch
"@nextui-org/shared-utils": patch
---

Fixed incorrect year in `showMonthAndYearPickers` with different locales
35 changes: 35 additions & 0 deletions packages/components/calendar/__tests__/calendar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {render, act, fireEvent} from "@testing-library/react";
import {CalendarDate, isWeekend} from "@internationalized/date";
import {triggerPress, keyCodes} from "@nextui-org/test-utils";
import {useLocale} from "@react-aria/i18n";
import {NextUIProvider} from "@nextui-org/system";

import {Calendar as CalendarBase, CalendarProps} from "../src";

Expand All @@ -16,6 +17,20 @@ const Calendar = React.forwardRef((props: CalendarProps, ref: React.Ref<HTMLDivE

Calendar.displayName = "Calendar";

const CalendarWithLocale = React.forwardRef(
(props: CalendarProps & {locale: string}, ref: React.Ref<HTMLDivElement>) => {
const {locale, ...otherProps} = props;

return (
<NextUIProvider locale={locale}>
<CalendarBase {...otherProps} ref={ref} disableAnimation />
</NextUIProvider>
);
},
);

CalendarWithLocale.displayName = "CalendarWithLocale";

describe("Calendar", () => {
beforeAll(() => {
jest.useFakeTimers();
Expand Down Expand Up @@ -428,5 +443,25 @@ describe("Calendar", () => {

expect(description).toBe("Selected date unavailable.");
});

it("should display the correct year and month in showMonthAndYearPickers with locale", () => {
const {getByRole} = render(
<CalendarWithLocale
showMonthAndYearPickers
defaultValue={new CalendarDate(2024, 6, 26)}
locale="th-TH-u-ca-buddhist"
/>,
);

const header = document.querySelector<HTMLButtonElement>(`button[data-slot="header"]`)!;

triggerPress(header);

const month = getByRole("button", {name: "มิถุนายน"});
const year = getByRole("button", {name: "พ.ศ. 2567"});

expect(month).toHaveAttribute("data-value", "6");
expect(year).toHaveAttribute("data-value", "2567");
});
});
});
21 changes: 15 additions & 6 deletions packages/components/calendar/src/use-calendar-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ import type {SupportedCalendars} from "@nextui-org/system";
import type {CalendarState, RangeCalendarState} from "@react-stately/calendar";
import type {RefObject, ReactNode} from "react";

import {Calendar, CalendarDate} from "@internationalized/date";
import {createCalendar, Calendar, CalendarDate, DateFormatter} from "@internationalized/date";
import {mapPropsVariants, useProviderContext} from "@nextui-org/system";
import {useCallback, useMemo} from "react";
import {calendar} from "@nextui-org/theme";
import {useControlledState} from "@react-stately/utils";
import {ReactRef, useDOMRef} from "@nextui-org/react-utils";
import {useLocale} from "@react-aria/i18n";
import {clamp, dataAttr, objectToDeps} from "@nextui-org/shared-utils";
import {clamp, dataAttr, objectToDeps, getGregorianYearOffset} from "@nextui-org/shared-utils";
import {mergeProps} from "@react-aria/utils";

type NextUIBaseProps = Omit<HTMLNextUIProps<"div">, keyof AriaCalendarPropsBase | "onChange">;
Expand Down Expand Up @@ -183,6 +183,15 @@ export function useCalendarBase(originalProps: UseCalendarBasePropsComplete) {

const globalContext = useProviderContext();

const {locale} = useLocale();

const calendarProp = createCalendar(new DateFormatter(locale).resolvedOptions().calendar);

// by default, we are using gregorian calendar with possible years in [1900, 2099]
// however, some locales such as `th-TH-u-ca-buddhist` using different calendar making the years out of bound
// hence, add the corresponding offset to make sure the year is within the bound
const gregorianYearOffset = getGregorianYearOffset(calendarProp.identifier);

const {
ref,
as,
Expand All @@ -198,9 +207,11 @@ export function useCalendarBase(originalProps: UseCalendarBasePropsComplete) {
isHeaderExpanded: isHeaderExpandedProp,
isHeaderDefaultExpanded,
onHeaderExpandedChange = () => {},
minValue = globalContext?.defaultDates?.minDate ?? new CalendarDate(1900, 1, 1),
maxValue = globalContext?.defaultDates?.maxDate ?? new CalendarDate(2099, 12, 31),
createCalendar: createCalendarProp = globalContext?.createCalendar ?? null,
minValue = globalContext?.defaultDates?.minDate ??
new CalendarDate(calendarProp, 1900 + gregorianYearOffset, 1, 1),
maxValue = globalContext?.defaultDates?.maxDate ??
new CalendarDate(calendarProp, 2099 + gregorianYearOffset, 12, 31),
prevButtonProps: prevButtonPropsProp,
nextButtonProps: nextButtonPropsProp,
errorMessage,
Expand Down Expand Up @@ -239,8 +250,6 @@ export function useCalendarBase(originalProps: UseCalendarBasePropsComplete) {
const hasMultipleMonths = visibleMonths > 1;
const shouldFilterDOMProps = typeof Component === "string";

const {locale} = useLocale();

const slots = useMemo(
() =>
calendar({
Expand Down
22 changes: 15 additions & 7 deletions packages/components/date-input/src/use-date-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,14 @@ import type {DOMAttributes, GroupDOMAttributes} from "@react-types/shared";
import type {DateInputGroupProps} from "./date-input-group";

import {useLocale} from "@react-aria/i18n";
import {CalendarDate} from "@internationalized/date";
import {createCalendar, CalendarDate, DateFormatter} from "@internationalized/date";
import {mergeProps} from "@react-aria/utils";
import {PropGetter, useProviderContext} from "@nextui-org/system";
import {HTMLNextUIProps, mapPropsVariants} from "@nextui-org/system";
import {useDOMRef} from "@nextui-org/react-utils";
import {useDateField as useAriaDateField} from "@react-aria/datepicker";
import {useDateFieldState} from "@react-stately/datepicker";
import {createCalendar} from "@internationalized/date";
import {objectToDeps, clsx, dataAttr} from "@nextui-org/shared-utils";
import {objectToDeps, clsx, dataAttr, getGregorianYearOffset} from "@nextui-org/shared-utils";
import {dateInput} from "@nextui-org/theme";
import {useMemo} from "react";

Expand Down Expand Up @@ -116,6 +115,15 @@ export function useDateInput<T extends DateValue>(originalProps: UseDateInputPro

const [props, variantProps] = mapPropsVariants(originalProps, dateInput.variantKeys);

const {locale} = useLocale();

const calendarProp = createCalendar(new DateFormatter(locale).resolvedOptions().calendar);

// by default, we are using gregorian calendar with possible years in [1900, 2099]
// however, some locales such as `th-TH-u-ca-buddhist` using different calendar making the years out of bound
// hence, add the corresponding offset to make sure the year is within the bound
const gregorianYearOffset = getGregorianYearOffset(calendarProp.identifier);

const {
ref,
as,
Expand All @@ -134,8 +142,10 @@ export function useDateInput<T extends DateValue>(originalProps: UseDateInputPro
descriptionProps: descriptionPropsProp,
validationBehavior = globalContext?.validationBehavior ?? "aria",
shouldForceLeadingZeros = true,
minValue = globalContext?.defaultDates?.minDate ?? new CalendarDate(1900, 1, 1),
maxValue = globalContext?.defaultDates?.maxDate ?? new CalendarDate(2099, 12, 31),
minValue = globalContext?.defaultDates?.minDate ??
new CalendarDate(calendarProp, 1900 + gregorianYearOffset, 1, 1),
maxValue = globalContext?.defaultDates?.maxDate ??
new CalendarDate(calendarProp, 2099 + gregorianYearOffset, 12, 31),
createCalendar: createCalendarProp = globalContext?.createCalendar ?? null,
isInvalid: isInvalidProp = validationState ? validationState === "invalid" : false,
errorMessage,
Expand All @@ -146,8 +156,6 @@ export function useDateInput<T extends DateValue>(originalProps: UseDateInputPro

const disableAnimation = originalProps.disableAnimation ?? globalContext?.disableAnimation;

const {locale} = useLocale();

const state = useDateFieldState({
...originalProps,
label,
Expand Down
49 changes: 49 additions & 0 deletions packages/components/date-picker/__tests__/date-picker.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {render, act, fireEvent, waitFor} from "@testing-library/react";
import {pointerMap, triggerPress} from "@nextui-org/test-utils";
import userEvent from "@testing-library/user-event";
import {CalendarDate, CalendarDateTime} from "@internationalized/date";
import {NextUIProvider} from "@nextui-org/system";

import {DatePicker as DatePickerBase, DatePickerProps} from "../src";

Expand All @@ -24,6 +25,26 @@ const DatePicker = React.forwardRef((props: DatePickerProps, ref: React.Ref<HTML

DatePicker.displayName = "DatePicker";

const DatePickerWithLocale = React.forwardRef(
(props: DatePickerProps & {locale: string}, ref: React.Ref<HTMLDivElement>) => {
const {locale, ...otherProps} = props;

return (
<NextUIProvider locale={locale}>
<DatePickerBase
{...otherProps}
ref={ref}
disableAnimation
labelPlacement="outside"
shouldForceLeadingZeros={false}
/>
</NextUIProvider>
);
},
);

DatePickerWithLocale.displayName = "DatePickerWithLocale";

function getTextValue(el: any) {
if (
el.className?.includes?.("DatePicker-placeholder") &&
Expand Down Expand Up @@ -626,5 +647,33 @@ describe("DatePicker", () => {
// assert that the second datepicker dialog is open
expect(dialog).toBeVisible();
});

it("should display the correct year and month in showMonthAndYearPickers with locale", () => {
const {getByRole} = render(
<DatePickerWithLocale
showMonthAndYearPickers
defaultValue={new CalendarDate(2024, 6, 26)}
label="Date"
locale="th-TH-u-ca-buddhist"
/>,
);

const button = getByRole("button");

triggerPress(button);

const dialog = getByRole("dialog");
const header = document.querySelector<HTMLButtonElement>(`button[data-slot="header"]`)!;

expect(dialog).toBeVisible();

triggerPress(header);

const month = getByRole("button", {name: "มิถุนายน"});
const year = getByRole("button", {name: "พ.ศ. 2567"});

expect(month).toHaveAttribute("data-value", "6");
expect(year).toHaveAttribute("data-value", "2567");
});
});
});
8 changes: 3 additions & 5 deletions packages/core/system/src/provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {I18nProvider, I18nProviderProps} from "@react-aria/i18n";
import {RouterProvider} from "@react-aria/utils";
import {OverlayProvider} from "@react-aria/overlays";
import {useMemo} from "react";
import {CalendarDate} from "@internationalized/date";
import {MotionGlobalConfig} from "framer-motion";

import {ProviderContext} from "./provider-context";
Expand Down Expand Up @@ -42,10 +41,9 @@ export const NextUIProvider: React.FC<NextUIProviderProps> = ({
skipFramerMotionAnimations = disableAnimation,
validationBehavior = "aria",
locale = "en-US",
defaultDates = {
minDate: new CalendarDate(1900, 1, 1),
maxDate: new CalendarDate(2099, 12, 31),
},
// if minDate / maxDate are not specified in `defaultDates`
// then they will be set in `use-date-input.ts` or `use-calendar-base.ts`
defaultDates,
createCalendar,
...otherProps
}) => {
Expand Down
26 changes: 26 additions & 0 deletions packages/utilities/shared-utils/src/dates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export function getGregorianYearOffset(identifier: string): number {
switch (identifier) {
case "buddhist":
return 543;
case "ethiopic":
case "ethioaa":
return -8;
case "coptic":
return -284;
case "hebrew":
return 3760;
case "indian":
return -78;
case "islamic-civil":
case "islamic-tbla":
case "islamic-umalqura":
return -579;
case "persian":
return 622;
case "roc":
case "japanese":
case "gregory":
default:
return 0;
}
}
1 change: 1 addition & 0 deletions packages/utilities/shared-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export * from "./functions";
export * from "./numbers";
export * from "./console";
export * from "./types";
export * from "./dates";

0 comments on commit f5d94f9

Please sign in to comment.