mirror of
https://github.com/nextui-org/nextui.git
synced 2025-12-08 19:26:11 +00:00
fix: incorrect year in showMonthAndYearPickers with locale (#3331)
* 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
This commit is contained in:
parent
fd4b7200dd
commit
f5d94f96e4
8
.changeset/purple-singers-knock.md
Normal file
8
.changeset/purple-singers-knock.md
Normal file
@ -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
|
||||
@ -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";
|
||||
|
||||
@ -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();
|
||||
@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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">;
|
||||
@ -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,
|
||||
@ -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,
|
||||
@ -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({
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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,
|
||||
@ -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,
|
||||
@ -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,
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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") &&
|
||||
@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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";
|
||||
@ -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
|
||||
}) => {
|
||||
|
||||
26
packages/utilities/shared-utils/src/dates.ts
Normal file
26
packages/utilities/shared-utils/src/dates.ts
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
@ -7,3 +7,4 @@ export * from "./functions";
|
||||
export * from "./numbers";
|
||||
export * from "./console";
|
||||
export * from "./types";
|
||||
export * from "./dates";
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user